Upstream gookit/goutil state of the touched files before 949a07a2cc

This commit is contained in:
upstream committed 2026-09-04 09:07:10 +02:00
1 parent d6c91f3cdd
commit b17a55589a
3 files changed
+382 -74

No files matched your search

-74
View File
@@ -1,74 +0,0 @@
# fix(strutil): BaseConv family returns "0" for zero instead of empty string
## Problem
Converting the value zero through the base-conversion helpers returns an empty
string instead of `"0"`:
```go
strutil.BaseConv("0", 10, 16) // "" , want "0"
strutil.BaseConv("0", 16, 10) // "" , want "0"
strutil.Base10Conv("0", 62) // "" , want "0"
strutil.BaseConvInt(0, 64) // "" , want "0"
```
This is also internally inconsistent: `BaseConvInt` returns the correct `"0"`
for small bases because it delegates to `strconv.FormatUint`/`big.Int.Text`,
but returns `""` for bases in `(62, 64]`, which go through the template path:
```go
strutil.BaseConvInt(0, 16) // "0" (strconv path)
strutil.BaseConvInt(0, 64) // "" (template path)
```
## Root cause
`strutil/convbase.go`, `BaseConvIntByTpl`:
```go
func BaseConvIntByTpl(dec uint64, toBase string) string {
var res string
toLen := uint64(len(toBase))
for dec > 0 { // never runs when dec == 0
res = string(toBase[dec%toLen]) + res
dec /= toLen
}
return res // returns "" for dec == 0
}
```
The loop condition `dec > 0` is never entered when `dec == 0`, so the function
returns the zero-value empty string. Every public entry point that reaches the
template path (`BaseConvIntByTpl`, `BaseConvByTpl`, `BaseConv`, `Base10Conv`,
`BaseConvInt` for base > 62) inherits the defect.
## Fix
Handle the zero case explicitly by emitting the zero digit of the target base
(the first character of the template), and guard against an empty template to
avoid an out-of-range index:
```go
if toLen == 0 {
return res
}
if dec == 0 {
return string(toBase[0])
}
```
## Test
Added `TestBaseConv_zero` in `strutil/convbase_test.go` covering `BaseConv`,
`Base10Conv`, `BaseConvInt` (small and large bases) and `BaseConvIntByTpl`.
- Gate: `go test ./strutil/ -run 'TestBaseConv|TestBase10Conv'` -> ok (PASS)
- `go vet ./strutil/` -> exit 0
- `go build ./...` -> exit 0
Counterfactual: reverting the change in `BaseConvIntByTpl` makes the new test
fail with `actual: string("")` vs `expect: string("0")`.
Note: `TestDateSNv3/dateLen4_base62` in `strutil/gensn_test.go` is flaky and
already fails on the unmodified base commit (it asserts uniqueness of randomly
generated serial numbers over 60 iterations); it is unrelated to this change.
+221
View File
@@ -0,0 +1,221 @@
package strutil
import (
"strings"
"github.com/gookit/goutil/comdef"
)
// PosFlag type
type PosFlag = comdef.Position
// Position for padding/resize string
const (
PosLeft PosFlag = iota
PosMiddle
PosRight
PosAuto
)
/*************************************************************
* String padding operation
*************************************************************/
// Padding Fill the string to the specified length.
//
// params:
// - s: 原始字符串
// - pad: 用于填充的字符或字符串
// - length: 目标长度
// - padPos: 填充位置标志(左填充或右填充)
func Padding(s, pad string, length int, padPos PosFlag) string {
return padding(s, pad, len(s), length, padPos)
}
// Utf8Padding Fill the string to the specified length. use utf8 width.
func Utf8Padding(s, pad string, wantLen int, padPos PosFlag) string {
return padding(s, pad, Utf8Width(s), wantLen, padPos)
}
func padding(s, pad string, sLen, wantLen int, padPos PosFlag) string {
diff := sLen - wantLen
if diff >= 0 { // do not need padding.
return s
}
// pad space.
if len(s) == sLen && (pad == "" || pad == " ") {
// Sprintf: 是按字数来填充的,不管中英文都是一个字符 - 有问题
if padPos == PosRight { // to right
return s + strings.Repeat(" ", -diff)
}
return strings.Repeat(" ", -diff) + s
}
// other character.
if padPos == PosRight { // to right
return s + Repeat(pad, -diff)
}
return Repeat(pad, -diff) + s
}
// PadLeft a string.
func PadLeft(s, pad string, length int) string {
return Padding(s, pad, length, PosLeft)
}
// PadRight a string.
func PadRight(s, pad string, length int) string {
return Padding(s, pad, length, PosRight)
}
// PadChars padding a rune/byte to want length and with position flag
func PadChars[T byte | rune](cs []T, pad T, length int, pos PosFlag) []T {
ln := len(cs)
if ln >= length {
ns := make([]T, length)
copy(ns, cs[:length])
return ns
}
idx := length - ln
ns := make([]T, length)
if pos == PosRight {
copy(ns, cs)
for i := ln; i < length; i++ {
ns[i] = pad
}
return ns
}
// to left
for i := 0; i < idx; i++ {
ns[i] = pad
}
copy(ns[idx:], cs)
return ns
}
// PadBytes padding a byte to want length and with position flag
func PadBytes(bs []byte, pad byte, length int, pos PosFlag) []byte {
return PadChars(bs, pad, length, pos)
}
// PadBytesLeft a byte to want length
func PadBytesLeft(bs []byte, pad byte, length int) []byte {
return PadChars(bs, pad, length, PosLeft)
}
// PadBytesRight a byte to want length
func PadBytesRight(bs []byte, pad byte, length int) []byte {
return PadChars(bs, pad, length, PosRight)
}
// PadRunes padding a rune to want length and with position flag
func PadRunes(rs []rune, pad rune, length int, pos PosFlag) []rune {
return PadChars(rs, pad, length, pos)
}
// PadRunesLeft a rune to want length
func PadRunesLeft(rs []rune, pad rune, length int) []rune {
return PadChars(rs, pad, length, PosLeft)
}
// PadRunesRight a rune to want length
func PadRunesRight(rs []rune, pad rune, length int) []rune {
return PadChars(rs, pad, length, PosRight)
}
// Align a string by given length and align settings. alias of Resize
func Align(s string, length int, align comdef.Align) string {
return resize(s, len(s), length, align, false)
}
// Utf8Align a string by given length and align settings. alias of Utf8Resize
func Utf8Align(s string, length int, align comdef.Align) string {
return resize(s, Utf8Width(s), length, align, false)
}
// Resize a string by given length and align settings. use padding space.
// If len(s) > wantLen, will truncate it.
func Resize(s string, length int, align comdef.Align) string {
return resize(s, len(s), length, align, true)
}
// Utf8Resize a string by given length and align settings. use padding space.
// If width(s) > wantLen, will truncate it.
func Utf8Resize(s string, length int, align comdef.Align) string {
return resize(s, Utf8Width(s), length, align, true)
}
// resize a string by given length and align settings. use padding space.
func resize(s string, sLen, wantLen int, align comdef.Align, cutOverflow bool) string {
diff := sLen - wantLen
if diff >= 0 { // do not need padding.
// cutOverflow: truncate on sLen > wantLen
if cutOverflow && sLen > wantLen {
if len(s) == sLen {
return s[:wantLen]
}
return utf8Truncate(s, sLen, wantLen, "")
}
return s
}
if align == comdef.Center {
padLn := (wantLen - sLen) / 2
if diff = wantLen - padLn*2; diff > 0 {
s += " "
}
padStr := string(RepeatBytes(' ', padLn))
return padStr + s + padStr
}
padStr := string(RepeatBytes(' ', wantLen-sLen))
// tip: 左对齐 - 使用空白填充右边
if align == comdef.Left || align == comdef.PosAuto {
return s + padStr
}
return padStr + s
}
/*************************************************************
* String repeat operation
*************************************************************/
// Repeat a string by given times.
func Repeat(s string, times int) string {
if times < 1 {
return ""
}
if times == 1 {
return s
}
var sb strings.Builder
sb.Grow(len(s) * times)
for i := 0; i < times; i++ {
sb.WriteString(s)
}
return sb.String()
}
// RepeatRune repeat a rune char.
func RepeatRune(char rune, times int) []rune { return RepeatChars(char, times) }
// RepeatBytes repeat a byte char.
func RepeatBytes(char byte, times int) []byte { return RepeatChars(char, times) }
// RepeatChars repeat a byte char.
func RepeatChars[T byte | rune](char T, times int) []T {
if times <= 0 {
return make([]T, 0)
}
chars := make([]T, times)
for i := 0; i < times; i++ {
chars[i] = char
}
return chars
}
+161
View File
@@ -0,0 +1,161 @@
package strutil_test
import (
"fmt"
"testing"
"github.com/gookit/goutil/comdef"
"github.com/gookit/goutil/strutil"
"github.com/gookit/goutil/x/assert"
)
func TestPadding(t *testing.T) {
tests := []struct {
want, give, pad string
len int
pos strutil.PosFlag
}{
{"ab000", "ab", "0", 5, strutil.PosRight},
{"000ab", "ab", "0", 5, strutil.PosLeft},
{"ab012", "ab012", "0", 4, strutil.PosLeft},
{"ab ", "ab", "", 5, strutil.PosRight},
{" ab", "ab", "", 5, strutil.PosLeft},
}
for _, tt := range tests {
assert.Eq(t, tt.want, strutil.Padding(tt.give, tt.pad, tt.len, tt.pos))
if tt.pos == strutil.PosRight {
assert.Eq(t, tt.want, strutil.PadRight(tt.give, tt.pad, tt.len))
} else {
assert.Eq(t, tt.want, strutil.PadLeft(tt.give, tt.pad, tt.len))
}
}
s := "hi,你好"
assert.Eq(t, "hi,你好0", strutil.Padding(s, "0", 10, strutil.PosRight))
assert.Eq(t, "hi,你好 ", strutil.Padding(s, " ", 10, strutil.PosRight))
assert.Eq(t, "hi,你好000", strutil.Utf8Padding(s, "0", 10, strutil.PosRight))
}
func TestResize(t *testing.T) {
tests := []struct {
want, give string
len int
align strutil.PosFlag
}{
{"ab ", "ab", 5, strutil.PosLeft},
{" ab", "ab", 5, strutil.PosRight},
{"ab012", "ab012", 5, strutil.PosLeft},
{"ab", "ab", 2, strutil.PosLeft},
}
for _, tt := range tests {
assert.Eq(t, tt.want, strutil.Resize(tt.give, tt.len, tt.align))
}
want := " title "
assert.Eq(t, want, strutil.Resize("title", 20, strutil.PosMiddle))
s := "hi,你好"
fmt.Println(len(s), strutil.Utf8Width(s))
assert.Eq(t, "hi,你好 ", strutil.Align(s, 10, strutil.PosLeft))
assert.Eq(t, "hi,你好 ", strutil.Resize(s, 10, strutil.PosLeft))
// utf8
assert.Eq(t, "hi,你好 ", strutil.Utf8Align(s, 10, strutil.PosAuto))
assert.Eq(t, "hi,你好 ", strutil.Utf8Resize(s, 10, comdef.PosAuto))
// long string
s1 := "hi,你好 some 内容太长"
assert.Eq(t, s1, strutil.Align(s1, 13, strutil.PosLeft))
assert.Eq(t, "hi,你好 some ", strutil.Resize(s1, 15, strutil.PosLeft))
assert.Eq(t, s1, strutil.Utf8Align(s1, 13, strutil.PosLeft))
assert.Eq(t, "hi,你好 some 内", strutil.Utf8Resize(s1, 15, comdef.Left))
}
func TestRepeat(t *testing.T) {
assert.Eq(t, "aaa", strutil.Repeat("a", 3))
assert.Eq(t, "DD", strutil.Repeat("D", 2))
assert.Eq(t, "D", strutil.Repeat("D", 1))
assert.Eq(t, "", strutil.Repeat("A", 0))
assert.Eq(t, "", strutil.Repeat("D", -3))
}
func TestRepeatRune(t *testing.T) {
tests := []struct {
want []rune
give rune
times int
}{
{[]rune("bbb"), 'b', 3},
{[]rune("..."), '.', 3},
{[]rune(" "), ' ', 2},
}
for _, tt := range tests {
assert.Eq(t, tt.want, strutil.RepeatRune(tt.give, tt.times))
}
}
func TestRepeatBytes(t *testing.T) {
assert.Eq(t, []byte("aaa"), strutil.RepeatBytes('a', 3))
assert.Eq(t, []byte{}, strutil.RepeatBytes('a', 0))
assert.Eq(t, []byte{}, strutil.RepeatBytes(' ', 0))
assert.Eq(t, []byte{}, strutil.RepeatBytes(' ', -2))
assert.Eq(t, []byte{' '}, strutil.RepeatBytes(' ', 1))
assert.Len(t, strutil.RepeatBytes(' ', 20), 20)
}
func TestPadChars(t *testing.T) {
tests := []struct {
wt []byte
ls []byte
pad byte
pln int
}{
{
[]byte("aaaabc"), []byte("abc"), 'a', 6,
},
{
[]byte("abc"), []byte("abcd"), 'a', 3,
},
}
for _, item := range tests {
assert.Eq(t, item.wt, strutil.PadChars(item.ls, item.pad, item.pln, strutil.PosLeft))
assert.Eq(t, item.wt, strutil.PadBytes(item.ls, item.pad, item.pln, strutil.PosLeft))
assert.Eq(t, item.wt, strutil.PadBytesLeft(item.ls, item.pad, item.pln))
}
tests2 := []struct {
wt []byte
ls []byte
pad byte
pln int
}{
{
[]byte("abcaaa"), []byte("abc"), 'a', 6,
},
{
[]byte("abc"), []byte("abcd"), 'a', 3,
},
}
for _, item := range tests2 {
assert.Eq(t, item.wt, strutil.PadChars(item.ls, item.pad, item.pln, strutil.PosRight))
assert.Eq(t, item.wt, strutil.PadBytes(item.ls, item.pad, item.pln, strutil.PosRight))
assert.Eq(t, item.wt, strutil.PadBytesRight(item.ls, item.pad, item.pln))
}
}
func TestPadRunes(t *testing.T) {
assert.Eq(t, []rune("hi123aaa"), strutil.PadRunesRight([]rune("hi123"), 'a', 8))
assert.Eq(t, []rune("aaahi123"), strutil.PadRunesLeft([]rune("hi123"), 'a', 8))
assert.Eq(t, []rune("hi123aaa"), strutil.PadRunes([]rune("hi123"), 'a', 8, strutil.PosRight))
}
// runtime error: make slice: cap out of range #76
// https://github.com/gookit/goutil/issues/76
func TestIssues_76(t *testing.T) {
// TODO
}