44package format
55
66import (
7- "fmt"
8- "reflect"
97 "strconv"
10- "strings "
8+ "unsafe "
119
1210 "go.followtheprocess.codes/cli/internal/constraints"
1311)
@@ -17,6 +15,17 @@ const (
1715 floatFmt = 'g'
1816 floatPrecision = - 1
1917 slice = "[]"
18+
19+ // Capacity hints used to pre-size []byte buffers in the slice formatters.
20+ // The "brackets" pair is the leading '[' and trailing ']'.
21+ //
22+ // The per-element hints are good enough default cases to minimise the
23+ // buffer growing and re-allocating.
24+ bracketsCap = 2
25+ intElemHint = 4 // "-12, "
26+ floatElemHint = 8 // "-1.234, "
27+ boolElemHint = 7 // "false, "
28+ stringElemHint = 4 // surrounding quotes plus ", "
2029)
2130
2231const (
@@ -99,39 +108,145 @@ func Float64(f float64) string {
99108// Slice([]int{1, 2, 3, 4}) // "[1, 2, 3, 4]"
100109// Slice([]string{"one", "two", "three"}) // `["one", "two", "three"]`
101110func Slice [T any ](s []T ) string {
102- length := len (s )
111+ if len (s ) == 0 {
112+ return slice
113+ }
103114
104- if length == 0 {
105- // If it's empty or nil, avoid doing the work below
106- // and just return "[]"
115+ switch v := any (s ).(type ) {
116+ case []string :
117+ return formatStringSlice (v )
118+ case []bool :
119+ return formatBoolSlice (v )
120+ case []int :
121+ return formatSignedSlice (v )
122+ case []int8 :
123+ return formatSignedSlice (v )
124+ case []int16 :
125+ return formatSignedSlice (v )
126+ case []int32 :
127+ return formatSignedSlice (v )
128+ case []int64 :
129+ return formatSignedSlice (v )
130+ case []uint :
131+ return formatUnsignedSlice (v )
132+ case []uint16 :
133+ return formatUnsignedSlice (v )
134+ case []uint32 :
135+ return formatUnsignedSlice (v )
136+ case []uint64 :
137+ return formatUnsignedSlice (v )
138+ case []float32 :
139+ return formatFloat32Slice (v )
140+ case []float64 :
141+ return formatFloat64Slice (v )
142+ default :
107143 return slice
108144 }
145+ }
146+
147+ // toString casts b to a string by reinterpreting the bytes.
148+ //
149+ // This is the same trick [strings.Builder.String] uses to avoid the
150+ // allocation of doing `string(b)`. The caveat is b MUST not be mutated
151+ // after passing to this function.
152+ //
153+ // This is fine in our case here as the []byte buffer is created in the function body
154+ // and never escapes.
155+ func toString (b []byte ) string {
156+ return unsafe .String (unsafe .SliceData (b ), len (b ))
157+ }
158+
159+ func formatSignedSlice [T constraints.Signed ](s []T ) string {
160+ buf := make ([]byte , 0 , bracketsCap + len (s )* intElemHint )
161+ buf = append (buf , '[' )
162+ buf = strconv .AppendInt (buf , int64 (s [0 ]), base10 )
163+
164+ for _ , e := range s [1 :] {
165+ buf = append (buf , ", " ... )
166+ buf = strconv .AppendInt (buf , int64 (e ), base10 )
167+ }
168+
169+ buf = append (buf , ']' )
170+
171+ return toString (buf )
172+ }
173+
174+ func formatUnsignedSlice [T constraints.Unsigned ](s []T ) string {
175+ buf := make ([]byte , 0 , bracketsCap + len (s )* intElemHint )
176+ buf = append (buf , '[' )
177+ buf = strconv .AppendUint (buf , uint64 (s [0 ]), base10 )
178+
179+ for _ , e := range s [1 :] {
180+ buf = append (buf , ", " ... )
181+ buf = strconv .AppendUint (buf , uint64 (e ), base10 )
182+ }
109183
110- builder := & strings.Builder {}
111- builder .WriteByte ('[' )
184+ buf = append (buf , ']' )
112185
113- typ := reflect .TypeFor [T ]().Kind ()
186+ return toString (buf )
187+ }
188+
189+ func formatFloat32Slice (s []float32 ) string {
190+ buf := make ([]byte , 0 , bracketsCap + len (s )* floatElemHint )
191+ buf = append (buf , '[' )
192+ buf = strconv .AppendFloat (buf , float64 (s [0 ]), floatFmt , floatPrecision , bits32 )
114193
115- first := fmt . Sprintf ( "%v" , s [0 ])
116- if typ == reflect . String {
117- first = strconv .Quote ( first )
194+ for _ , e := range s [1 :] {
195+ buf = append ( buf , ", " ... )
196+ buf = strconv .AppendFloat ( buf , float64 ( e ), floatFmt , floatPrecision , bits32 )
118197 }
119198
120- builder .WriteString (first )
199+ buf = append (buf , ']' )
200+
201+ return toString (buf )
202+ }
203+
204+ func formatFloat64Slice (s []float64 ) string {
205+ buf := make ([]byte , 0 , bracketsCap + len (s )* floatElemHint )
206+ buf = append (buf , '[' )
207+ buf = strconv .AppendFloat (buf , s [0 ], floatFmt , floatPrecision , bits64 )
121208
122- for _ , element := range s [1 :] {
123- builder .WriteString (", " )
209+ for _ , e := range s [1 :] {
210+ buf = append (buf , ", " ... )
211+ buf = strconv .AppendFloat (buf , e , floatFmt , floatPrecision , bits64 )
212+ }
213+
214+ buf = append (buf , ']' )
215+
216+ return toString (buf )
217+ }
218+
219+ func formatStringSlice (s []string ) string {
220+ capacity := bracketsCap
221+ for _ , e := range s {
222+ capacity += len (e ) + stringElemHint
223+ }
224+
225+ buf := make ([]byte , 0 , capacity )
226+ buf = append (buf , '[' )
227+ buf = strconv .AppendQuote (buf , s [0 ])
228+
229+ for _ , e := range s [1 :] {
230+ buf = append (buf , ", " ... )
231+ buf = strconv .AppendQuote (buf , e )
232+ }
233+
234+ buf = append (buf , ']' )
235+
236+ return toString (buf )
237+ }
124238
125- str := fmt .Sprintf ("%v" , element )
126- if typ == reflect .String {
127- // If it's a string, quote it
128- str = strconv .Quote (str )
129- }
239+ func formatBoolSlice (s []bool ) string {
240+ buf := make ([]byte , 0 , bracketsCap + len (s )* boolElemHint )
241+ buf = append (buf , '[' )
242+ buf = strconv .AppendBool (buf , s [0 ])
130243
131- builder .WriteString (str )
244+ for _ , e := range s [1 :] {
245+ buf = append (buf , ", " ... )
246+ buf = strconv .AppendBool (buf , e )
132247 }
133248
134- builder . WriteByte ( ']' )
249+ buf = append ( buf , ']' )
135250
136- return builder . String ( )
251+ return toString ( buf )
137252}
0 commit comments