From b17a55589ac0941db406699b997e692b79721333 Mon Sep 17 00:00:00 2001 From: upstream Date: Fri, 4 Sep 2026 09:07:10 +0200 Subject: [PATCH] Upstream gookit/goutil state of the touched files before 949a07a2cc --- PR.md | 74 -------------- strutil/padding.go | 221 ++++++++++++++++++++++++++++++++++++++++ strutil/padding_test.go | 161 +++++++++++++++++++++++++++++ 3 files changed, 382 insertions(+), 74 deletions(-) delete mode 100644 PR.md create mode 100644 strutil/padding.go create mode 100644 strutil/padding_test.go diff --git a/PR.md b/PR.md deleted file mode 100644 index 4c26d1c..0000000 --- a/PR.md +++ /dev/null @@ -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. diff --git a/strutil/padding.go b/strutil/padding.go new file mode 100644 index 0000000..cfb01d1 --- /dev/null +++ b/strutil/padding.go @@ -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 +} diff --git a/strutil/padding_test.go b/strutil/padding_test.go new file mode 100644 index 0000000..3d2351d --- /dev/null +++ b/strutil/padding_test.go @@ -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 +}