1
0
mirror of https://github.com/duke-git/lancet.git synced 2026-03-01 00:35:28 +08:00

Compare commits

...

5 Commits

Author SHA1 Message Date
dudaodong
27667f8b3a fix: fix bugs in test 2025-04-21 15:08:16 +08:00
dudaodong
0b5dc86d70 refactor: refact some strutil functions 2025-04-21 14:07:52 +08:00
dudaodong
d88bba07dd feat: add TryKeyedLocker 2025-04-21 10:49:48 +08:00
dudaodong
d7f3354b98 feat: add RWKeyedLocker 2025-04-17 14:25:19 +08:00
dudaodong
f4dee28ebb feat: add KeyedLocker 2025-04-17 14:06:59 +08:00
10 changed files with 620 additions and 140 deletions

250
concurrency/keyed_locker.go Normal file
View File

@@ -0,0 +1,250 @@
// Copyright 2025 dudaodong@gmail.com. All rights reserved.
// Use of this source code is governed by MIT license
// Package concurrency contain some functions to support concurrent programming. eg, goroutine, channel, locker.
package concurrency
import (
"context"
"sync"
"sync/atomic"
"time"
)
// KeyedLocker is a simple implementation of a keyed locker that allows for non-blocking lock acquisition.
type KeyedLocker[K comparable] struct {
locks sync.Map
ttl time.Duration
}
type lockEntry struct {
mu sync.Mutex
ref int32
timer atomic.Pointer[time.Timer]
}
// NewKeyedLocker creates a new KeyedLocker with the specified TTL for lock expiration.
// The TTL is used to automatically release locks that are no longer held.
func NewKeyedLocker[K comparable](ttl time.Duration) *KeyedLocker[K] {
return &KeyedLocker[K]{ttl: ttl}
}
// Do acquires a lock for the specified key and executes the provided function.
// It returns an error if the context is canceled before the function completes.
func (l *KeyedLocker[K]) Do(ctx context.Context, key K, fn func()) error {
entry := l.acquire(key)
defer l.release(key, entry, key)
done := make(chan struct{})
go func() {
entry.mu.Lock()
defer entry.mu.Unlock()
select {
case <-ctx.Done():
default:
fn()
}
close(done)
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-done:
return nil
}
}
// acquire tries to acquire a lock for the specified key.
func (l *KeyedLocker[K]) acquire(key K) *lockEntry {
lock, _ := l.locks.LoadOrStore(key, &lockEntry{})
entry := lock.(*lockEntry)
atomic.AddInt32(&entry.ref, 1)
if t := entry.timer.Swap(nil); t != nil {
t.Stop()
}
return entry
}
// release releases the lock for the specified key.
func (l *KeyedLocker[K]) release(key K, entry *lockEntry, rawKey K) {
if atomic.AddInt32(&entry.ref, -1) == 0 {
entry.mu.Lock()
defer entry.mu.Unlock()
if entry.ref == 0 {
if t := entry.timer.Swap(nil); t != nil {
t.Stop()
}
l.locks.Delete(rawKey)
} else {
if entry.timer.Load() == nil {
t := time.AfterFunc(l.ttl, func() {
l.release(key, entry, rawKey)
})
entry.timer.Store(t)
}
}
}
}
// RWKeyedLocker is a read-write version of KeyedLocker.
type RWKeyedLocker[K comparable] struct {
locks sync.Map
ttl time.Duration
}
type rwLockEntry struct {
mu sync.RWMutex
ref int32
timer atomic.Pointer[time.Timer]
}
// NewRWKeyedLocker creates a new RWKeyedLocker with the specified TTL for lock expiration.
// The TTL is used to automatically release locks that are no longer held.
func NewRWKeyedLocker[K comparable](ttl time.Duration) *RWKeyedLocker[K] {
return &RWKeyedLocker[K]{ttl: ttl}
}
// RLock acquires a read lock for the specified key and executes the provided function.
// It returns an error if the context is canceled before the function completes.
func (l *RWKeyedLocker[K]) RLock(ctx context.Context, key K, fn func()) error {
entry := l.acquire(key)
defer l.release(entry, key)
done := make(chan struct{})
go func() {
entry.mu.RLock()
defer entry.mu.RUnlock()
select {
case <-ctx.Done():
default:
fn()
}
close(done)
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-done:
return nil
}
}
// Lock acquires a write lock for the specified key and executes the provided function.
// It returns an error if the context is canceled before the function completes.
func (l *RWKeyedLocker[K]) Lock(ctx context.Context, key K, fn func()) error {
entry := l.acquire(key)
defer l.release(entry, key)
done := make(chan struct{})
go func() {
entry.mu.Lock()
defer entry.mu.Unlock()
select {
case <-ctx.Done():
default:
fn()
}
close(done)
}()
select {
case <-ctx.Done():
return ctx.Err()
case <-done:
return nil
}
}
// acquire tries to acquire a read lock for the specified key.
func (l *RWKeyedLocker[K]) acquire(key K) *rwLockEntry {
actual, _ := l.locks.LoadOrStore(key, &rwLockEntry{})
entry := actual.(*rwLockEntry)
atomic.AddInt32(&entry.ref, 1)
if t := entry.timer.Swap(nil); t != nil {
t.Stop()
}
return entry
}
// release releases the lock for the specified key.
func (l *RWKeyedLocker[K]) release(entry *rwLockEntry, rawKey K) {
if atomic.AddInt32(&entry.ref, -1) == 0 {
timer := time.AfterFunc(l.ttl, func() {
if atomic.LoadInt32(&entry.ref) == 0 {
l.locks.Delete(rawKey)
}
})
entry.timer.Store(timer)
}
}
// TryKeyedLocker is a non-blocking version of KeyedLocker.
// It allows for trying to acquire a lock without blocking if the lock is already held.
type TryKeyedLocker[K comparable] struct {
mu sync.Mutex
locks map[K]*casMutex
}
// NewTryKeyedLocker creates a new TryKeyedLocker.
func NewTryKeyedLocker[K comparable]() *TryKeyedLocker[K] {
return &TryKeyedLocker[K]{locks: make(map[K]*casMutex)}
}
// TryLock tries to acquire a lock for the specified key.
// It returns true if the lock was acquired, false otherwise.
func (l *TryKeyedLocker[K]) TryLock(key K) bool {
l.mu.Lock()
lock, ok := l.locks[key]
if !ok {
lock = &casMutex{}
l.locks[key] = lock
}
l.mu.Unlock()
return lock.TryLock()
}
// Unlock releases the lock for the specified key.
func (l *TryKeyedLocker[K]) Unlock(key K) {
l.mu.Lock()
defer l.mu.Unlock()
lock, ok := l.locks[key]
if ok {
lock.Unlock()
if lock.lock == 0 {
delete(l.locks, key)
}
}
}
// casMutex is a simple mutex that uses atomic operations to provide a non-blocking lock.
type casMutex struct {
lock int32
}
// TryLock tries to acquire the lock without blocking.
// It returns true if the lock was acquired, false otherwise.
func (m *casMutex) TryLock() bool {
return atomic.CompareAndSwapInt32(&m.lock, 0, 1)
}
// Unlock releases the lock.
func (m *casMutex) Unlock() {
atomic.StoreInt32(&m.lock, 0)
}

View File

@@ -0,0 +1,230 @@
package concurrency
import (
"context"
"strconv"
"sync"
"testing"
"time"
"github.com/duke-git/lancet/v2/internal"
)
func TestKeyedLocker_SerialExecutionSameKey(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestKeyedLocker_SerialExecutionSameKey")
locker := NewKeyedLocker[string](100 * time.Millisecond)
var result []int
var mu sync.Mutex
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
err := locker.Do(context.Background(), "key1", func() {
time.Sleep(10 * time.Millisecond)
mu.Lock()
defer mu.Unlock()
result = append(result, i)
})
assert.IsNil(err)
}(i)
}
wg.Wait()
assert.Equal(5, len(result))
}
func TestKeyedLocker_ParallelExecutionDifferentKeys(t *testing.T) {
locker := NewKeyedLocker[string](100 * time.Millisecond)
start := time.Now()
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
key := "key" + strconv.Itoa(i)
locker.Do(context.Background(), key, func() {
time.Sleep(50 * time.Millisecond)
})
}(i)
}
wg.Wait()
elapsed := time.Since(start)
if elapsed > 100*time.Millisecond {
t.Errorf("parallel execution took too long: %s", elapsed)
}
}
func TestKeyedLocker_ContextTimeout(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestKeyedLocker_ContextTimeout")
locker := NewKeyedLocker[string](100 * time.Millisecond)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
defer cancel()
// Lock key before calling
go func() {
_ = locker.Do(context.Background(), "key-timeout", func() {
time.Sleep(50 * time.Millisecond)
})
}()
time.Sleep(1 * time.Millisecond) // ensure lock is acquired first
err := locker.Do(ctx, "key-timeout", func() {
t.Error("should not execute")
})
assert.IsNotNil(err)
}
func TestKeyedLocker_LockReleaseAfterTTL(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestKeyedLocker_LockReleaseAfterTTL")
locker := NewKeyedLocker[string](50 * time.Millisecond)
err := locker.Do(context.Background(), "ttl-key", func() {})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// Wait for TTL to pass
time.Sleep(100 * time.Millisecond)
err = locker.Do(context.Background(), "ttl-key", func() {})
assert.IsNil(err)
}
func TestRWKeyedLocker_LockAndUnlock(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestKeyedLocker_LockReleaseAfterTTL")
locker := NewRWKeyedLocker[string](500 * time.Millisecond)
var locked bool
err := locker.Lock(context.Background(), "key1", func() {
locked = true
})
assert.IsNil(err)
assert.Equal(true, locked)
}
func TestRWKeyedLocker_RLockParallel(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestKeyedLocker_LockReleaseAfterTTL")
locker := NewRWKeyedLocker[string](1 * time.Second)
var mu sync.Mutex
var count int
wg := sync.WaitGroup{}
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
err := locker.RLock(context.Background(), "shared-key", func() {
time.Sleep(10 * time.Millisecond)
mu.Lock()
count++
mu.Unlock()
})
assert.IsNil(err)
}()
}
wg.Wait()
assert.Equal(5, count)
}
func TestRWKeyedLocker_LockTimeout(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestRWKeyedLocker_LockTimeout")
locker := NewRWKeyedLocker[string](1 * time.Second)
start := make(chan struct{})
go func() {
locker.Lock(context.Background(), "key-timeout", func() {
close(start)
time.Sleep(200 * time.Millisecond)
})
}()
<-start
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
err := locker.Lock(ctx, "key-timeout", func() {
t.Error("should not reach here")
})
assert.IsNotNil(err)
}
func TestTryKeyedLocker_SimpleLockUnlock(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestTryKeyedLocker_SimpleLockUnlock")
locker := NewTryKeyedLocker[string]()
ok := locker.TryLock("key1")
assert.Equal(true, ok)
ok = locker.TryLock("key1")
assert.Equal(false, ok)
locker.Unlock("key1")
ok = locker.TryLock("key1")
assert.Equal(true, ok)
locker.Unlock("key1")
}
func TestTryKeyedLocker_ParallelTry(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestTryKeyedLocker_ParallelTry")
locker := NewTryKeyedLocker[string]()
var wg sync.WaitGroup
var mu sync.Mutex
var count int
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
ok := locker.TryLock("key" + strconv.Itoa(i))
mu.Lock()
if ok {
count++
}
mu.Unlock()
time.Sleep(10 * time.Millisecond)
if ok {
locker.Unlock("key" + strconv.Itoa(i))
}
}(i)
}
wg.Wait()
assert.Equal(5, count)
assert.Equal(0, len(locker.locks))
}

View File

@@ -1,51 +1,51 @@
-----BEGIN rsa private key-----
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
-----END rsa private key-----

View File

@@ -1,14 +1,14 @@
-----BEGIN rsa public key-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8NkRHdZhqGYZ6tkWy3x
jkm7PlRqi2yS+AgguuuWhw3DvhM4BYwKt+r7Z0aEK9uIqpi2GwLv475kt+sQlPFb
W/qVEM2RE9X4kZFhc56ZfomeUq/puUd6Yz4Ba7JkXsk1ErwtMbzf+eNPlCA9idBZ
Ofccy6nXHQkGsQQO9o8AM9jdOfhI8x/8CqJss2IZjQZxlzc37Rqs+OVD2S8S2Dr6
QUPyD/2P1dCwm4imw7BX3BMcwVg/LCQIGJIoMQ16T6prA/c6ShtnxHhc2ZyJ8ito
7Bs9v7+9yGd6w6Img/I0QCbFwGbXFSHLypFV2XauzFZbGQg7WM2WuKuU6NvRx1/E
I/DMXUIXRNvAlgXYbJc1V+x/2718MuuUaQjw39xderTkhU26wL2Rr/WfUaHQRq7F
alaPohpw9P5awm2dZGRo+mw+KmzWMf/Yb2h11oVBeeHK1H/FoDBR8tq519QT2dSn
U4teIXqfT8KR40PZQ8IzIISyGoXi2uvlLIGrht6xweysyOAHHAaoDiX/ZeYDzhsd
cRzo6fih+JPs/AwbeoNVuTBb0FZslEXmB+rWdlCABWyvKjXqFaBOw966muBoDpvl
FhIXXT7cAhFN072yaDNa3judlY9yiBAWQQsv2Qr05YJIfdS3nQ3bqIu3BIwOhv/t
HCfUANM3DosKam4kxEIydWECAwEAAQ==
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtXr+2FbR6Prdf9U0gfq2
OJbTD82m84bbdeyfsakabYp8Haw6mZW6ebRNPJvQ87H6DbA55hK1E7y6m+wjvbUo
pnkGh75Cp04CoQvaU8Rs7MrQcJ2Zz1VRRgugmqwJMWE/FC5eGXvvpDjF2h2lqThK
ZTKcvhqfZ3dNX+6v4EQ4BbjSGKdzpyexzR1WSV9IzZYRHMk8jCoEHOx9ItEzoToB
f9yAq0DhufylqSRbjKLhR7A3IiRSLVNfu0Sz5cfNlS7eUA060QS8AV9BgGNTkl75
aj8BCOmoHIW85SsSZvdQWYwmlM76Jn+tfNNnY7Uuqtz2gI1odcy1nXembXWJryxu
YCkSAwpU8UNIQpZeKwBnBxaf0pj3W1m3UzEqhGkkCrKq8LkNSGZSkhT+bI65fNx2
LZ7lS165szkeyqS8aikxcBpdx7Z/bqbFFuI42KX+IpXAJCAtvsQ0XXinr83MLcY/
I2Ic/5Sgz7txfZsKKwKt48ETS9BjxGPWo1Dd5YjM8ZJWJv2Rf0mwVjUMClFSNBm8
x+aWGVIKjpocN/plpEZmFrWdPcWM4ZH3qGCEnVVkqPvqClahNSQXsM4W7msYts1I
Uq31tjBSV5bL6l2K8jFJa4PRYdzh1vHhDOAYTpgDkQbNaoDlDS2o/0TkAGUfXWJt
R3D35wRJzoa7bCMt5BuFE3sCAwEAAQ==
-----END rsa public key-----

View File

@@ -15,7 +15,6 @@ func TestSinglyLink_InsertAtFirst(t *testing.T) {
link.InsertAtHead(1)
link.InsertAtHead(2)
link.InsertAtHead(3)
link.Print()
expected := []int{3, 2, 1}
values := link.Values()
@@ -32,7 +31,6 @@ func TestSinglyLink_InsertAtTail(t *testing.T) {
link.InsertAtTail(1)
link.InsertAtTail(2)
link.InsertAtTail(3)
link.Print()
expected := []int{1, 2, 3}
values := link.Values()
@@ -77,7 +75,6 @@ func TestSinglyLink_DeleteAtHead(t *testing.T) {
link.InsertAtTail(4)
link.DeleteAtHead()
link.Print()
expected := []int{2, 3, 4}
values := link.Values()

View File

@@ -2,6 +2,7 @@ package eventbus
import (
"fmt"
"sort"
"sync"
"time"
)
@@ -224,6 +225,7 @@ func ExampleEventBus_GetEvents() {
eb.Subscribe("event2", func(eventData int) {}, false, 0, nil)
events := eb.GetEvents()
sort.Strings(events)
for _, event := range events {
fmt.Println(event)

View File

@@ -1,6 +1,7 @@
package eventbus
import (
"sort"
"sync"
"testing"
"time"
@@ -213,7 +214,8 @@ func TestEventBus_GetEvents(t *testing.T) {
eb.Subscribe("event2", func(eventData int) {}, false, 0, nil)
events := eb.GetEvents()
sort.Strings(events)
assert.Equal(2, len(events))
assert.EqualValues([]string{"event1", "event2"}, events)
assert.Equal([]string{"event1", "event2"}, events)
}

View File

@@ -3,7 +3,6 @@ package netutil
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
@@ -27,8 +26,8 @@ func TestHttpGet(t *testing.T) {
return
}
body, _ := io.ReadAll(resp.Body)
t.Log("response: ", resp.StatusCode, string(body))
defer resp.Body.Close()
t.Log("response status:", resp.StatusCode)
}
func TestHttpPost(t *testing.T) {
@@ -49,8 +48,8 @@ func TestHttpPost(t *testing.T) {
return
}
body, _ := io.ReadAll(resp.Body)
t.Log("response: ", resp.StatusCode, string(body))
defer resp.Body.Close()
t.Log("response status:", resp.StatusCode)
}
func TestHttpPostFormData(t *testing.T) {
@@ -69,8 +68,8 @@ func TestHttpPostFormData(t *testing.T) {
return
}
body, _ := io.ReadAll(resp.Body)
t.Log("response: ", resp.StatusCode, string(body))
defer resp.Body.Close()
t.Log("response status:", resp.StatusCode)
}
func TestHttpPut(t *testing.T) {
@@ -92,8 +91,8 @@ func TestHttpPut(t *testing.T) {
return
}
body, _ := io.ReadAll(resp.Body)
t.Log("response: ", resp.StatusCode, string(body))
defer resp.Body.Close()
t.Log("response status:", resp.StatusCode)
}
func TestHttpPatch(t *testing.T) {
@@ -115,8 +114,8 @@ func TestHttpPatch(t *testing.T) {
return
}
body, _ := io.ReadAll(resp.Body)
t.Log("response: ", resp.StatusCode, string(body))
defer resp.Body.Close()
t.Log("response status:", resp.StatusCode)
}
func TestHttpDelete(t *testing.T) {
@@ -127,8 +126,8 @@ func TestHttpDelete(t *testing.T) {
return
}
body, _ := io.ReadAll(resp.Body)
t.Log("response: ", resp.StatusCode, string(body))
defer resp.Body.Close()
t.Log("response status:", resp.StatusCode)
}
func TestConvertMapToQueryString(t *testing.T) {
@@ -229,8 +228,8 @@ func TestHttpClent_Post(t *testing.T) {
return
}
body, _ := io.ReadAll(resp.Body)
t.Log("response: ", resp.StatusCode, string(body))
defer resp.Body.Close()
t.Log("response status:", resp.StatusCode)
}
func TestStructToUrlValues(t *testing.T) {

View File

@@ -37,16 +37,17 @@ func CamelCase(s string) string {
// Capitalize converts the first character of a string to upper case and the remaining to lower case.
// Play: https://go.dev/play/p/2OAjgbmAqHZ
func Capitalize(s string) string {
result := make([]rune, len(s))
for i, v := range s {
if i == 0 {
result[i] = unicode.ToUpper(v)
} else {
result[i] = unicode.ToLower(v)
}
if s == "" {
return s
}
return string(result)
runes := []rune(s)
runes[0] = unicode.ToUpper(runes[0])
for i := 1; i < len(runes); i++ {
runes[i] = unicode.ToLower(runes[i])
}
return string(runes)
}
// UpperFirst converts the first character of string to upper case.
@@ -127,49 +128,57 @@ func UpperSnakeCase(s string) string {
// Before returns the substring of the source string up to the first occurrence of the specified string.
// Play: https://go.dev/play/p/JAWTZDS4F5w
func Before(s, char string) string {
i := strings.Index(s, char)
if s == "" || char == "" || i == -1 {
if char == "" {
return s
}
return s[0:i]
if i := strings.Index(s, char); i >= 0 {
return s[:i]
}
return s
}
// BeforeLast returns the substring of the source string up to the last occurrence of the specified string.
// Play: https://go.dev/play/p/pJfXXAoG_Te
func BeforeLast(s, char string) string {
i := strings.LastIndex(s, char)
if s == "" || char == "" || i == -1 {
if char == "" {
return s
}
return s[0:i]
if i := strings.LastIndex(s, char); i >= 0 {
return s[:i]
}
return s
}
// After returns the substring after the first occurrence of a specified string in the source string.
// Play: https://go.dev/play/p/RbCOQqCDA7m
func After(s, char string) string {
i := strings.Index(s, char)
if s == "" || char == "" || i == -1 {
if char == "" {
return s
}
return s[i+len(char):]
if i := strings.Index(s, char); i >= 0 {
return s[i+len(char):]
}
return s
}
// AfterLast returns the substring after the last occurrence of a specified string in the source string.
// Play: https://go.dev/play/p/1TegARrb8Yn
func AfterLast(s, char string) string {
i := strings.LastIndex(s, char)
if s == "" || char == "" || i == -1 {
if char == "" {
return s
}
return s[i+len(char):]
if i := strings.LastIndex(s, char); i >= 0 {
return s[i+len(char):]
}
return s
}
// IsString check if the value data type is string or not.
@@ -213,20 +222,15 @@ func Wrap(str string, wrapWith string) string {
// Unwrap a given string from anther string. will change source string.
// Play: https://go.dev/play/p/Ec2q4BzCpG-
func Unwrap(str string, wrapToken string) string {
if str == "" || wrapToken == "" {
if wrapToken == "" || !strings.HasPrefix(str, wrapToken) || !strings.HasSuffix(str, wrapToken) {
return str
}
firstIndex := strings.Index(str, wrapToken)
lastIndex := strings.LastIndex(str, wrapToken)
if firstIndex == 0 && lastIndex > 0 && lastIndex <= len(str)-1 {
if len(wrapToken) <= lastIndex {
str = str[len(wrapToken):lastIndex]
}
if len(str) < 2*len(wrapToken) {
return str
}
return str
return str[len(wrapToken) : len(str)-len(wrapToken)]
}
// SplitEx split a given string which can control the result slice contains empty string or not.
@@ -286,22 +290,21 @@ func Substring(s string, offset int, length uint) string {
size := len(rs)
if offset < 0 {
offset = size + offset
if offset < 0 {
offset = 0
}
offset += size
}
if offset < 0 {
offset = 0
}
if offset > size {
return ""
}
if length > uint(size)-uint(offset) {
length = uint(size - offset)
end := offset + int(length)
if end > size {
end = size
}
str := string(rs[offset : offset+int(length)])
return strings.Replace(str, "\x00", "", -1)
return strings.ReplaceAll(string(rs[offset:end]), "\x00", "")
}
// SplitWords splits a string into words, word only contains alphabetic characters.

View File

@@ -1,6 +1,7 @@
package strutil
import (
"strings"
"unicode"
)
@@ -111,31 +112,27 @@ func padAtPosition(str string, length int, padStr string, position int) string {
padStr = " "
}
length = length - len(str)
startPadLen := 0
totalPad := length - len(str)
startPad := 0
if position == 0 {
startPadLen = length / 2
startPad = totalPad / 2
} else if position == 1 {
startPadLen = length
startPad = totalPad
} else if position == 2 {
startPad = 0
}
endPadLen := length - startPadLen
endPad := totalPad - startPad
charLen := len(padStr)
leftPad := ""
cur := 0
for cur < startPadLen {
leftPad += string(padStr[cur%charLen])
cur++
repeatPad := func(n int) string {
repeated := strings.Repeat(padStr, (n+len(padStr)-1)/len(padStr))
return repeated[:n]
}
cur = 0
rightPad := ""
for cur < endPadLen {
rightPad += string(padStr[cur%charLen])
cur++
}
left := repeatPad(startPad)
right := repeatPad(endPad)
return leftPad + str + rightPad
return left + str + right
}
// isLetter checks r is a letter but not CJK character.