shad-go/varfmt/fmt.go

76 lines
2 KiB
Go

//go:build !solution
package varfmt
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
)
// fmt.Sprintf does 1 alloc
// this does 3-4 allocations and is a bit slower than fmt.Sprintf
// can be further optimized, by casting args slice
// to string without allocating new space (somehow)
func Sprintf(format string, args ...any) string {
// init output strings.Builder and num rune buffer
// cant use strings.Builder for storing num since
// builder can't clear its contents without reallocating
output, num := strings.Builder{}, make([]rune, 0, 2) // first alloc here
// convert args slice to strArgs string slice
strArgs, maxLen := make([]string, len(args), cap(args)), 0 // second alloc here
for i, arg := range args {
strArgs[i] = fmt.Sprint(arg) // third and other allocs happen here
// could be further optimized
if len(strArgs[i]) > maxLen {
maxLen = len(strArgs[i])
}
}
// allocate maximum possible length of the resulting string
output.Grow(len(format) + len(args)*maxLen) // third alloc here
start, argIndex := 0, 0
// decode runes one by one
for start < len(format) {
r, n := utf8.DecodeRuneInString(format[start:])
start += n
// if specifier is found, start forming index
if r == '{' {
numLength := 0
for {
r, n = utf8.DecodeRuneInString(format[start:])
start += n
if r == '}' {
break
}
numLength++
// append new rune if the number is bigger
// than the current buffer
if numLength >= len(num) {
num = append(num, r) // further allocs happen here
} else {
num[numLength] = r
}
}
var index int
// if specifier is empty get arg at argIndex
// otherwise get arg at the specified index from num
if len(num) == 0 {
index = argIndex
} else {
var err error
index, err = strconv.Atoi(string(num))
if err != nil {
panic(err)
}
}
output.WriteString(strArgs[index])
argIndex++
// set len of num to 0, but keep capacity
num = num[:0]
continue
}
output.WriteRune(r)
}
return output.String()
}