Compare commits

...
58 Commits
Author SHA1 Message Date
dudaodong 8dd57ef490 remove go report card 2026-10-31 10:30:11 +08:00
Rupayon Haldar 7e02d7e4e6 fix(package): [algorithm] keep LRU endpoint links consistent (#406) 2026-10-31 09:56:48 +08:00
Rupayon Haldar 9f7d939e5a fix(package): [tree] apply the replacement root on deletion (#407) 2026-10-31 09:56:26 +08:00
Rupayon Haldar 03695c886c fix(hashmap): preserve collision chains and bucket indexes (#408) 2026-10-31 09:54:49 +08:00
Rupayon Haldar 9c401c4043 fix(list): release the sort lock on every exit (#409) 2026-10-31 09:54:39 +08:00
Rupayon Haldar f7acd40303 fix(list): preserve receiver contents in SubList (#410) 2026-10-31 09:53:45 +08:00
Rupayon Haldar eb4b10cc15 fix(package): [link] preserve deletion bounds and backlinks (#405) 2026-10-31 09:53:29 +08:00
dudaodong eb9ad10f25 Merge branch 'rc' of github.com:duke-git/lancet into rc 2026-10-24 23:04:11 +08:00
dudaodong 22316061d8 release v2.4.0 2026-10-24 23:03:48 +08:00
Yusuf EfeandDuDaoDong 5774d0e95e fix(mathutil): compute permutations without factorial division (#364)
Co-authored-by: DuDaoDong <dudaodong@gmail.com>
2026-10-24 22:50:03 +08:00
Yusuf Efe 9555cb2fc5 fix(package): update LRU length after deletion (#359)
Signed-off-by: Yusuf Efe <120668197+yuefdev@users.noreply.github.com>
2026-10-24 22:46:07 +08:00
Yusuf Efe 9b0a0caa75 fix(mathutil): preserve distances at extreme scales (#360) 2026-10-23 22:43:47 +08:00
Yusuf Efe ec11fdf345 fix(mathutil): scale integer inputs after float conversion (#361) 2026-10-23 21:02:24 +08:00
Yusuf Efe 172712df30 fix(mathutil): honour Fibonacci seeds at the first positions (#362) 2026-10-23 20:57:07 +08:00
Yusuf Efe a296281408 fix(mathutil): format TruncRound integer inputs as floats (#365) 2026-10-23 20:54:59 +08:00
Yusuf Efe f220ea4c3f fix(slice): handle Intersection without inputs (#366) 2026-10-23 20:54:26 +08:00
Yusuf Efe 129da3f2ed fix(validator): exclude the correct GBK trail byte (#372) 2026-10-23 20:54:08 +08:00
Yusuf Efe 65806afc7d fix(maputil): map slices into array destinations (#371) 2026-10-23 20:53:52 +08:00
Yusuf Efe 4db163b4b7 fix(validator): correct JCB digit pattern escaping (#373) 2026-10-23 20:53:02 +08:00
Yusuf Efe 0f4f73fbba fix(mathutil): divide before multiplying LCM factors (#363) 2026-10-23 18:19:46 +08:00
Yusuf Efe 145959ee62 fix(strutil): encode default trim characters as runes (#369) 2026-10-23 18:14:58 +08:00
Yusuf Efe 33a7e5bf6d fix(strutil): validate Hamming distance by rune count (#368) 2026-10-23 18:14:41 +08:00
Yusuf Efe 99ed115de4 fix(strutil): preserve IndexOffset not-found results (#367) 2026-10-23 18:14:24 +08:00
Yusuf Efe 34c7e36d29 fix(slice): pass actual comparison indexes to DifferenceBy (#370) 2026-10-23 18:14:06 +08:00
Yusuf Efe 59cd9bd06f fix(validator): reject backslashes in Base64 values (#374) 2026-10-23 18:12:27 +08:00
Yusuf Efe 18e9bc9cda fix(maputil): validate MapToStruct inputs before reflection (#375) 2026-10-23 18:10:00 +08:00
Yusuf Efe b282ada39e fix(mathutil): stop stepped ranges when values stop increasing (#376) 2026-10-23 18:04:43 +08:00
Yusuf Efe b40eea78c3 fix(strutil): clamp substring lengths before integer conversion (#377) 2026-10-23 18:04:29 +08:00
Yusuf Efe c701a35586 fix(fileutil): preserve CSV fields and truncate replaced files (#378) 2026-10-23 18:04:11 +08:00
Yusuf Efe 778bd2e14f fix(fileutil): reject copies onto the source file (#379) 2026-10-23 18:03:37 +08:00
Yusuf Efe 86dc6c0019 fix(convertor): clone array values without slice reflection (#380) 2026-10-23 18:03:23 +08:00
Yusuf Efe 417cf68bd8 fix(package): [datetime] accept empty timezone options (#384) 2026-10-22 10:57:53 +08:00
Yusuf Efe cb45ef2d36 fix(package): [algorithm] keep duplicate CountSort entries (#387) 2026-10-22 10:56:56 +08:00
Yusuf Efe 3811d3db8e fix(package): [convertor] preserve cloned scalar types (#383) 2026-10-22 10:56:42 +08:00
Yusuf Efe e2524f4498 fix(package): [queue] check membership only in active slots (#390) 2026-10-22 10:56:27 +08:00
Yusuf Efe 7c77aa00fb fix(formatter): exclude leading plus signs from comma groups (#382) 2026-10-22 10:56:14 +08:00
Yusuf Efe eb3b1e33d2 fix(slice): return safely when DeleteAt receives no elements (#381) 2026-10-22 10:55:55 +08:00
Yusuf Efe 752f1ed63d fix(package): [datetime] count calendar days across DST (#385) 2026-10-22 10:48:53 +08:00
Yusuf Efe 5c04ffae97 fix(package): [datetime] reject non-positive intervals (#386) 2026-10-22 10:48:16 +08:00
Yusuf Efe f1db13387b fix(package): [netutil] reject failed download responses (#388) 2026-10-22 10:48:00 +08:00
Yusuf Efe be9b167a9b fix(package): [netutil] handle upload responses (#389) 2026-10-22 10:47:39 +08:00
Yusuf Efe febd1030af fix(package): [list] preserve source storage during merge (#391) 2026-10-22 10:47:25 +08:00
Yusuf Efe fe4c81a677 fix(package): [queue] keep circular queues reusable after clear (#394) 2026-10-22 10:43:42 +08:00
Yusuf Efe 8225b6d7d9 fix(package): [stream] avoid overflowing range arithmetic (#393) 2026-10-22 10:40:12 +08:00
Yusuf Efe 6ff2966d50 fix(package): [validator] match whole Chinese phone numbers (#396) 2026-10-22 10:39:56 +08:00
Yusuf Efe 70b8fc764e fix(package): [stream] bound Limit capacity by source length (#400) 2026-10-22 10:39:28 +08:00
Yusuf Efe f525703d9f fix(package): [slice] report unreadable struct fields (#401) 2026-10-22 10:39:00 +08:00
Yusuf Efe be3bc0c43f fix(package): [iterator] report available elements in HasNext (#392) 2026-10-22 10:34:04 +08:00
Yusuf Efe 9685357daf fix(package): [validator] accept supported hostless URL schemes (#395) 2026-10-22 10:31:04 +08:00
Yusuf Efe 9a8b67ac85 fix(package): [netutil] skip invalid forwarded IP values (#397) 2026-10-22 10:29:35 +08:00
Yusuf Efe 19ccb13893 fix(package): [random] handle non-positive unique slice lengths (#402) 2026-10-22 10:29:10 +08:00
Yusuf Efe ef22b5b339 fix(package): [structs] recognise directly embedded fields (#403) 2026-10-22 10:28:54 +08:00
Yusuf Efe 3b94ec1932 fix(package): [netutil] bracket IPv6 dial addresses (#398) 2026-10-22 10:28:40 +08:00
Yusuf Efe e49a01e725 fix(package): [algorithm] handle empty iterative searches (#399) 2026-10-22 10:28:04 +08:00
dudaodong 13b7b1c61e fix: use mock proxy in TestProxy 2026-10-13 11:09:26 +08:00
Copilotandduke-git 227501958f Fix fileutil CI panic by serving ReadFile test fixtures from a local httptest server (#358)
* Initial plan

* Apply remaining changes

Co-authored-by: duke-git <5037603+duke-git@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: duke-git <5037603+duke-git@users.noreply.github.com>
2026-10-13 11:07:23 +08:00
Sash 835111c832 fix: treat IPv6 and shared-space addresses as internal (#357)
IsInternalIP returned false for unique-local and link-local IPv6, for
100.64.0.0/10, and for NAT64, 6to4, and Teredo addresses that carry an
internal IPv4 address.

Fixes #356
2026-10-13 11:00:26 +08:00
dudaodong 747dddee8b doc: add docment for UpperCamelCase 2026-03-07 12:33:39 +08:00
69 changed files with 5836 additions and 4653 deletions

No files matched your search

+2167 -2168
View File
File diff suppressed because it is too large. Load diff
+2176 -2177
View File
File diff suppressed because it is too large. Load diff
+76
View File
@@ -0,0 +1,76 @@
package algorithm
import (
"strconv"
"testing"
)
func TestLRUCacheReuseAfterDeletingLastEntry(t *testing.T) {
cache := NewLRUCache[int, string](1)
cache.Put(1, "one")
if !cache.Delete(1) || cache.Len() != 0 {
t.Fatal("deleting the sole entry must empty the cache")
}
cache.Put(2, "two")
cache.Put(3, "three")
if cache.Len() != 1 {
t.Fatalf("Len() = %d, want capacity 1", cache.Len())
}
if _, ok := cache.Get(2); ok {
t.Fatal("the least recently used replacement entry was not evicted")
}
if value, ok := cache.Get(3); !ok || value != "three" {
t.Fatalf("Get(3) = %q, %v", value, ok)
}
}
func TestLRUCacheEmptyAndReuse(t *testing.T) {
for _, capacity := range []int{0, 1, 2, 4} {
t.Run(strconv.Itoa(capacity), func(t *testing.T) {
defer func() {
if value := recover(); value != nil {
t.Errorf("cache reuse panicked: %v", value)
}
}()
cache := NewLRUCache[int, int](capacity)
for round := 0; round < 4; round++ {
for i := 0; i < capacity+2; i++ {
cache.Put(i, round*10+i)
if cache.Len() > capacity {
t.Fatalf("Len() = %d exceeds capacity %d", cache.Len(), capacity)
}
}
for i := 0; i < capacity+2; i++ {
cache.Delete(i)
}
if cache.Len() != 0 {
t.Fatalf("Len() = %d after deleting every key", cache.Len())
}
}
})
}
}
func TestLRUCacheEndpointDeletionKeepsRecency(t *testing.T) {
cache := NewLRUCache[int, int](3)
cache.Put(1, 10)
cache.Put(2, 20)
cache.Put(3, 30)
cache.Delete(1)
cache.Delete(3)
cache.Put(4, 40)
cache.Put(5, 50)
cache.Get(2)
cache.Put(6, 60)
if cache.Len() != 3 {
t.Fatalf("Len() = %d, want 3", cache.Len())
}
if _, ok := cache.Get(4); ok {
t.Fatal("Get(2) should make 4 the least recently used entry")
}
for key, want := range map[int]int{2: 20, 5: 50, 6: 60} {
if got, ok := cache.Get(key); !ok || got != want {
t.Fatalf("Get(%d) = %d, %v; want %d", key, got, ok, want)
}
}
}
+7 -4
View File
@@ -77,6 +77,7 @@ func (l *LRUCache[K, V]) Delete(key K) bool {
if ok {
key := l.deleteNode(node)
delete(l.cache, key)
l.length = len(l.cache)
return true
}
l.length = len(l.cache)
@@ -101,12 +102,14 @@ func (l *LRUCache[K, V]) addNode(node *lruNode[K, V]) {
}
func (l *LRUCache[K, V]) deleteNode(node *lruNode[K, V]) K {
if node == l.tail {
l.tail = l.tail.pre
} else if node == l.head {
l.head = l.head.next
if node.pre == nil {
l.head = node.next
} else {
node.pre.next = node.next
}
if node.next == nil {
l.tail = node.pre
} else {
node.next.pre = node.pre
}
return node.key
+24
View File
@@ -32,3 +32,27 @@ func TestLRUCache(t *testing.T) {
_, ok = cache.Get(2)
assert.Equal(false, ok)
}
func TestLRUCacheDeleteLen(t *testing.T) {
t.Parallel()
cache := NewLRUCache[int, int](3)
for key := 1; key <= 3; key++ {
cache.Put(key, key)
}
for _, key := range []int{2, 1, 3} {
before := cache.Len()
if !cache.Delete(key) {
t.Fatalf("Delete(%d) = false, want true", key)
}
if got := cache.Len(); got != before-1 {
t.Fatalf("Len() after Delete(%d) = %d, want %d", key, got, before-1)
}
if cache.Delete(key) {
t.Fatalf("Delete(%d) = true for a missing key", key)
}
if got := cache.Len(); got != before-1 {
t.Fatalf("Len() after missing Delete(%d) = %d, want %d", key, got, before-1)
}
}
}
+3
View File
@@ -45,6 +45,9 @@ func BinarySearch[T any](sortedSlice []T, target T, lowIndex, highIndex int, com
// If not found return -1.
// Play: https://go.dev/play/p/Anozfr8ZLH3
func BinaryIterativeSearch[T any](sortedSlice []T, target T, lowIndex, highIndex int, comparator constraints.Comparator) int {
if len(sortedSlice) == 0 {
return -1
}
startIndex := lowIndex
endIndex := highIndex
+11
View File
@@ -0,0 +1,11 @@
package algorithm
import "testing"
func TestBinaryIterativeSearchEmpty(t *testing.T) {
for _, source := range [][]int{nil, {}} {
if got := BinaryIterativeSearch(source, 1, 0, 0, &intComparator{}); got != -1 {
t.Errorf("got %d, want -1", got)
}
}
}
+2 -1
View File
@@ -188,7 +188,8 @@ func CountSort[T any](slice []T, comparator constraints.Comparator) []T {
count := 0
for j := 0; j < size; j++ {
//slice[i] > slice[j]
if comparator.Compare(slice[i], slice[j]) == 1 {
order := comparator.Compare(slice[i], slice[j])
if order == 1 || (order == 0 && j < i) {
count++
}
}
+3
View File
@@ -199,6 +199,9 @@ func TestMergeSort(t *testing.T) {
func TestCountSort(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestCountSort")
assert.Equal([]int{1, 2, 2}, CountSort([]int{2, 1, 2}, &intComparator{}))
assert.Equal([]people{{"young", 10}, {"first", 20}, {"second", 20}},
CountSort([]people{{"first", 20}, {"young", 10}, {"second", 20}}, &peopleAgeComparator{}))
peoples := []people{
{Name: "a", Age: 20},
+23
View File
@@ -0,0 +1,23 @@
package convertor
import (
"testing"
"github.com/duke-git/lancet/v2/internal"
)
func TestDeepCloneScalarTypes(t *testing.T) {
assert := internal.NewAssert(t, "TestDeepCloneScalarTypes")
type label string
type counter int64
assert.Equal(label("value"), DeepClone(label("value")))
assert.Equal(counter(42), DeepClone(counter(42)))
assert.Equal(uintptr(42), DeepClone(uintptr(42)))
value := label("pointed")
cloned := DeepClone(&value)
assert.Equal(value, *cloned)
*cloned = "changed"
assert.Equal(label("pointed"), value)
var empty any
assert.Equal(&empty, DeepClone(&empty))
}
+15 -47
View File
@@ -16,49 +16,10 @@ func (c *cloner) clone(v reflect.Value) reflect.Value {
case reflect.Invalid:
return reflect.ValueOf(nil)
// bool
case reflect.Bool:
return reflect.ValueOf(v.Bool())
//int
case reflect.Int:
return reflect.ValueOf(int(v.Int()))
case reflect.Int8:
return reflect.ValueOf(int8(v.Int()))
case reflect.Int16:
return reflect.ValueOf(int16(v.Int()))
case reflect.Int32:
return reflect.ValueOf(int32(v.Int()))
case reflect.Int64:
return reflect.ValueOf(v.Int())
// uint
case reflect.Uint:
return reflect.ValueOf(uint(v.Uint()))
case reflect.Uint8:
return reflect.ValueOf(uint8(v.Uint()))
case reflect.Uint16:
return reflect.ValueOf(uint16(v.Uint()))
case reflect.Uint32:
return reflect.ValueOf(uint32(v.Uint()))
case reflect.Uint64:
return reflect.ValueOf(v.Uint())
// float
case reflect.Float32:
return reflect.ValueOf(float32(v.Float()))
case reflect.Float64:
return reflect.ValueOf(v.Float())
// complex
case reflect.Complex64:
return reflect.ValueOf(complex64(v.Complex()))
case reflect.Complex128:
return reflect.ValueOf(v.Complex())
// string
case reflect.String:
return reflect.ValueOf(v.String())
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String:
return v
// array
case reflect.Array, reflect.Slice:
@@ -90,11 +51,16 @@ func (c *cloner) clone(v reflect.Value) reflect.Value {
}
func (c *cloner) cloneArray(v reflect.Value) reflect.Value {
if v.IsNil() {
if v.Kind() == reflect.Slice && v.IsNil() {
return reflect.Zero(v.Type())
}
arr := reflect.MakeSlice(v.Type(), v.Len(), v.Len())
var arr reflect.Value
if v.Kind() == reflect.Array {
arr = reflect.New(v.Type()).Elem()
} else {
arr = reflect.MakeSlice(v.Type(), v.Len(), v.Len())
}
for i := 0; i < v.Len(); i++ {
val := c.clone(v.Index(i))
@@ -179,8 +145,10 @@ func (c *cloner) clonePtr(v reflect.Value) reflect.Value {
}
}
clonedPtr := reflect.New(newVal.Type())
clonedPtr.Elem().Set(newVal)
clonedPtr := reflect.New(v.Type().Elem())
if newVal.IsValid() {
clonedPtr.Elem().Set(newVal)
}
return clonedPtr
}
+12
View File
@@ -394,6 +394,18 @@ func TestDecodeByte(t *testing.T) {
assert.Equal("abc", obj)
}
func TestDeepCloneArrays(t *testing.T) {
assert := internal.NewAssert(t, "TestDeepCloneArrays")
value := 42
type pair [2]*int
src := pair{&value, nil}
got := DeepClone(src)
assert.Equal(src, got)
*got[0] = 99
assert.Equal(42, *src[0])
assert.Equal([0]int{}, DeepClone([0]int{}))
}
func TestDeepClone(t *testing.T) {
t.Parallel()
+49 -49
View File
@@ -1,51 +1,51 @@
-----BEGIN rsa private key-----
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
-----END rsa private key-----
+12 -12
View File
@@ -1,14 +1,14 @@
-----BEGIN rsa public key-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuzBz+aC+e7Lvny2zYlcy
AfG6AAtPkxZqJ9JkYkM+0CP87pe0xOXQh4dz9iJekOwAq7FKpasUEUzkTm6Z0PUo
j/TWY/xpoPNXXzz/5dz3u6r/A5TumJ6BmX/7K/x8FsokIeP+lWaN1l+7uBKK8rgf
m4AZOXd/plzBkTrnu6lKG/rH9cnr2leKWDqk2jcG6r15r/07MdStpWgt0OBYoHzv
jLJWmJ08VrnF9PFtWhL939xSAIicFzJ0T9fAdmSSYmg22mKgN1zeWtZndJ/Ejv+5
YlWmuFJ8YKvwR0+4XIRLX9qsMy7qPTh7zsZhgtKzyb7qvVqDh2pmwekGJcwxGcoC
LHLqNKf/dD+4vXxgS+f1ObfOcJpDqBajD/U6BXtrZ/p4cvoYnZA383YR/CRG/nJ8
jIvt2FutT4hsNSd0L6c2mfo5mTnoDOEL3mkreZ4Az+GE57jMw1Ia9jwkM1QQoy0a
+kTiW8BedNqcRnVVGT9/OS/ggyKFwxJ/Xh1DfxZXAuyCBRUJUyVl9YCr2y30zngu
CdaTTViA9UbrjtcE2bZtnqOMAU2s08F0IiaGLKKMhrxUoXLgngXSX7gomC4aEfcg
5hf7ft6FA+bXB9DHwGdv/UyrGr88nve5um1OT5kmyOujKpka4QZ5/rU+RznBE0UW
DcHAyc+Zv+te0DqPUNcAW5ECAwEAAQ==
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4TSGNCTws7pRURptw911
vSr9rISCcjSgpjxVFtxkDpri/L+QHwDaDpTnAx2FKfFkBbYNklSl3JtY4vZQ8hND
vfHbwt+PagbNaeiu5l1agl7s2ssWSXQ2dIKx45gtQxsuvAVGvmdgVEuyt/UwJrHg
cyt5cqYejtRI5rcj509Pxegb3B84gkPwgy6xb2jYWBZ+pi6aLdm2TGX8SCsbCJcO
5SD3GFI4fG9OhsSaToD0/OSYzIbQ8WP6D7KX3y5tVHFqV6eFHT2ZIskDRzN8z6cD
xp+K2QcCtv5F7yqnjVBYbnk0dmC/woAaNMgvpSaC94cv0oFOS1TWivZDgpLkUJL2
hu4V1xGflkHVAdz/DlSUIfiJ47sLHOHnjBQXY81ruh10Xwf7Sh0P9TfNod0KITSZ
aqXljYaomEfMOXHn8t1xxy6mAF/cZf/nSmXn+r3ue+cYHxPD0sHYQQqCCAAsNO/V
T0Z1NXzjLwc497LwrfXnin0J2EGDyNQf2LKCj8JhICRw72tMrzp8XAHodfxWNQzP
L26k7ZzIGUUmlMn7ku0WcU2BsiGnSXNPlFmeOqOzfdmgmiERbQPd3uzZ3a7pxhWl
KGZSakZPEJeZHDEobmm+uBkr4OzIgc/jBNp3xa3wXiPxftzoPB3TpUQdN/mgW/De
HMS7V56ie3vqYZQG1vUC7rsCAwEAAQ==
-----END rsa public key-----
@@ -0,0 +1,138 @@
package datastructure_test
import (
"fmt"
"reflect"
"testing"
hashmap "github.com/duke-git/lancet/v2/datastructure/hashmap"
)
type collisionKey string
func TestHashMapCollisionStorage(t *testing.T) {
cases := map[string]func(*testing.T){
"distinct colliding keys": func(t *testing.T) {
h := hashmap.NewHashMapWithCapacity(0, 1)
keys := []any{1, "1", collisionKey("1")}
for i, k := range keys {
h.Put(k, i+10)
}
for i, k := range keys {
if h.Get(k) != i+10 || !h.Contains(k) {
t.Fatalf("lost key %T(%v): %v", k, k, h.Get(k))
}
}
if h.Size() != 3 {
t.Fatalf("size=%d", h.Size())
}
},
"overwrite cardinality": func(t *testing.T) {
h := hashmap.NewHashMap()
h.Put("same", 1)
h.Put("same", 2)
if h.Size() != 1 || !reflect.DeepEqual(h.Keys(), []any{"same"}) || !reflect.DeepEqual(h.Values(), []any{2}) {
t.Fatalf("overwrite size/keys/values: %d %v %v", h.Size(), h.Keys(), h.Values())
}
},
"delete absent colliding key": func(t *testing.T) {
h := hashmap.NewHashMap()
h.Put(1, "kept")
h.Delete("1")
if h.Size() != 1 || h.Get(1) != "kept" {
t.Fatalf("absent delete removed another key: size=%d get=%v", h.Size(), h.Get(1))
}
},
"delete every chain position": func(t *testing.T) {
for _, remove := range []any{1, "1", collisionKey("1")} {
h := hashmap.NewHashMapWithCapacity(0, 4)
keys := []any{1, "1", collisionKey("1")}
for i, k := range keys {
h.Put(k, i)
}
h.Delete(remove)
if h.Contains(remove) || h.Size() != 2 {
t.Fatalf("deletion failed: %T size=%d", remove, h.Size())
}
for i, k := range keys {
if !reflect.DeepEqual(k, remove) && h.Get(k) != i {
t.Fatalf("delete lost other key: %T", k)
}
}
}
},
"delete preserves bucket indexes": func(t *testing.T) {
h := hashmap.NewHashMap()
for i := 0; i < 100; i++ {
h.Put(fmt.Sprintf("key-%d", i), i)
}
for i := 0; i < 100; i += 2 {
h.Delete(fmt.Sprintf("key-%d", i))
}
for i := 1; i < 100; i += 2 {
if h.Get(fmt.Sprintf("key-%d", i)) != i {
t.Fatalf("lost untouched bucket key %d", i)
}
}
if h.Size() != 50 {
t.Fatalf("size=%d", h.Size())
}
},
"resize and enumerate chains": func(t *testing.T) {
h := hashmap.NewHashMapWithCapacity(0, 1)
h.Put(1, 1)
h.Put("1", 2)
h.Put(collisionKey("1"), 3)
for i := 2; i <= 64; i++ {
h.Put(i, i)
}
seen := map[string]any{}
h.Iterate(func(k, v any) { seen[fmt.Sprintf("%T:%v", k, k)] = v })
if h.Size() != 66 || len(seen) != 66 || len(h.Keys()) != 66 || len(h.Values()) != 66 {
t.Fatalf("size=%d enumerated=%d", h.Size(), len(seen))
}
if seen["string:1"] != 2 || h.Get(collisionKey("1")) != 3 {
t.Fatal("collision chain lost during growth")
}
},
"filter colliding values": func(t *testing.T) {
h := hashmap.NewHashMapWithCapacity(0, 4)
h.Put(1, 1)
h.Put("1", 2)
h.Put(collisionKey("1"), 3)
f := h.FilterByValue(func(v any) bool { return v.(int)%2 == 1 })
if f == nil || f.Size() != 2 || f.Get(1) != 1 || f.Get(collisionKey("1")) != 3 || f.Contains("1") {
t.Fatal("filter skipped or corrupted collision chain")
}
if h.FilterByValue(func(any) bool { return false }) != nil {
t.Fatal("empty-filter nil contract changed")
}
},
"zero capacity and deep equal keys": func(t *testing.T) {
h := hashmap.NewHashMapWithCapacity(0, 0)
if h.Get("missing") != nil || h.Contains("missing") {
t.Fatal("empty lookup")
}
h.Delete("missing")
h.Put([]int{1, 2}, "first")
h.Put([]int{1, 2}, "second")
if h.Get([]int{1, 2}) != "second" || h.Size() != 1 {
t.Fatal("deep equal overwrite failed")
}
h.Delete([]int{1, 2})
if h.Size() != 0 {
t.Fatal("deep equal delete failed")
}
},
}
for name, fn := range cases {
t.Run(name, func(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("public map operation panicked: %v", r)
}
}()
fn(t)
})
}
}
+52 -49
View File
@@ -44,6 +44,9 @@ func NewHashMapWithCapacity(size, capacity uint64) *HashMap {
// Get return the value of given key in hashmap
func (hm *HashMap) Get(key any) any {
if hm.size == 0 {
return nil
}
hashValue := hm.hash(key)
node := hm.table[hashValue]
for node != nil {
@@ -66,41 +69,55 @@ func (hm *HashMap) GetOrDefault(key any, defaultValue any) any {
// Put new key value in hashmap
func (hm *HashMap) Put(key any, value any) {
hm.putValue(hm.hash(key), key, value)
}
func (hm *HashMap) putValue(hash uint64, key, value any) {
if hm.capacity == 0 {
hm.capacity = defaultMapCapacity
hm.table = make([]*mapNode, defaultMapCapacity)
}
hm.putValue(hm.hash(key), key, value)
}
node := hm.table[hash]
if node == nil {
hm.table[hash] = newMapNode(key, value)
} else if node.key == key {
hm.table[hash] = newMapNodeWithNext(key, value, node)
} else {
hm.resize()
hm.putValue(hash, value, value)
func (hm *HashMap) putValue(hash uint64, key, value any) {
for node := hm.table[hash]; node != nil; node = node.next {
if reflect.DeepEqual(node.key, key) {
node.value = value
return
}
}
if hm.size >= hm.capacity {
hm.resize()
hash = hm.hash(key)
}
hm.table[hash] = newMapNodeWithNext(key, value, hm.table[hash])
hm.size++
}
// Delete item by given key in hashmap
func (hm *HashMap) Delete(key any) {
hash := hm.hash(key)
node := hm.table[hash]
if node == nil {
if hm.size == 0 {
return
}
hm.table = append(hm.table[:hash], hm.table[hash+1:]...)
hm.size--
hash := hm.hash(key)
var previous *mapNode
for node := hm.table[hash]; node != nil; node = node.next {
if reflect.DeepEqual(node.key, key) {
if previous == nil {
hm.table[hash] = node.next
} else {
previous.next = node.next
}
hm.size--
return
}
previous = node
}
}
// Contains checks if given key is in hashmap or not
func (hm *HashMap) Contains(key any) bool {
if hm.size == 0 {
return false
}
node := hm.table[hm.hash(key)]
for node != nil {
if reflect.DeepEqual(node.key, key) {
@@ -113,12 +130,10 @@ func (hm *HashMap) Contains(key any) bool {
// Iterate executes iteratee funcation for every key and value pair of hashmap (random order)
func (hm *HashMap) Iterate(iteratee func(key, value any)) {
if hm.size > 0 {
for i := 0; i < len(hm.table); i++ {
item := hm.table[i]
if item != nil {
iteratee(item.key, item.value)
}
for _, node := range hm.table {
for node != nil {
iteratee(node.key, node.value)
node = node.next
}
}
}
@@ -128,17 +143,14 @@ func (hm *HashMap) Iterate(iteratee func(key, value any)) {
// otherwise it returns the HashMap with selected values.
func (hm *HashMap) FilterByValue(perdicate func(value any) bool) *HashMap {
var filteredHM *HashMap
if hm.size > 0 {
for i := 0; i < len(hm.table); i++ {
item := hm.table[i]
if item != nil && perdicate(item.value) {
if filteredHM == nil {
filteredHM = NewHashMap()
}
filteredHM.Put(item.key, item.value)
hm.Iterate(func(key, value any) {
if perdicate(value) {
if filteredHM == nil {
filteredHM = NewHashMap()
}
filteredHM.Put(key, value)
}
}
})
return filteredHM
}
@@ -172,18 +184,16 @@ func (hm *HashMap) Values() []any {
func (hm *HashMap) resize() {
hm.capacity <<= 1
tempTable := hm.table
hm.table = make([]*mapNode, hm.capacity)
for i := 0; i < len(tempTable); i++ {
node := tempTable[i]
if node == nil {
continue
for _, node := range tempTable {
for node != nil {
next := node.next
hash := hm.hash(node.key)
node.next = hm.table[hash]
hm.table[hash] = node
node = next
}
hm.table[hm.hash(node.key)] = node
}
}
@@ -201,13 +211,6 @@ func (hm *HashMap) hash(key any) uint64 {
return (hm.capacity - 1) & (hashValue ^ (hashValue >> 16))
}
func newMapNode(key, value any) *mapNode {
return &mapNode{
key: key,
value: value,
}
}
func newMapNodeWithNext(key, value any, next *mapNode) *mapNode {
return &mapNode{
key: key,
+10 -7
View File
@@ -87,6 +87,7 @@ func (dl *DoublyLink[T]) InsertAt(index int, value T) {
newNode := datastructure.NewLinkNode(value)
newNode.Next = current.Next
newNode.Pre = current
current.Next.Pre = newNode
current.Next = newNode
dl.length++
@@ -106,7 +107,9 @@ func (dl *DoublyLink[T]) DeleteAtHead() {
current := dl.Head
dl.Head = current.Next
dl.Head.Pre = nil
if dl.Head != nil {
dl.Head.Pre = nil
}
dl.length--
}
@@ -119,6 +122,7 @@ func (dl *DoublyLink[T]) DeleteAtTail() {
current := dl.Head
if current.Next == nil {
dl.DeleteAtHead()
return
}
for current.Next.Next != nil {
@@ -132,27 +136,26 @@ func (dl *DoublyLink[T]) DeleteAtTail() {
// DeleteAt delete value in doubly linklist at index
// param `index` should be [0, len(DoublyLink)-1]
func (dl *DoublyLink[T]) DeleteAt(index int) {
if dl.Head == nil {
if dl.Head == nil || index < 0 || index >= dl.length {
return
}
current := dl.Head
if current.Next == nil || index == 0 {
if index == 0 {
dl.DeleteAtHead()
return
}
if index == dl.length-1 {
dl.DeleteAtTail()
}
if index < 0 || index > dl.length-1 {
return
}
current := dl.Head
i := 0
for current != nil {
if i == index-1 {
current.Next = current.Next.Next
current.Next.Pre = current
dl.length--
return
}
@@ -0,0 +1,115 @@
package datastructure
import (
"reflect"
"testing"
)
type mutableIntLink interface {
InsertAtHead(int)
InsertAtTail(int)
DeleteAtHead()
DeleteAtTail()
DeleteAt(int)
Reverse()
Values() []int
Size() int
IsEmpty() bool
}
func TestLinkDeletionBoundaries(t *testing.T) {
for name, newLink := range map[string]func() mutableIntLink{
"singly": func() mutableIntLink { return NewSinglyLink[int]() },
"doubly": func() mutableIntLink { return NewDoublyLink[int]() },
} {
t.Run(name, func(t *testing.T) {
for _, tt := range []struct {
name string
size int
delete func(mutableIntLink)
want []int
}{
{"empty head", 0, func(l mutableIntLink) { l.DeleteAtHead() }, []int{}},
{"empty tail", 0, func(l mutableIntLink) { l.DeleteAtTail() }, []int{}},
{"empty index", 0, func(l mutableIntLink) { l.DeleteAt(0) }, []int{}},
{"singleton head", 1, func(l mutableIntLink) { l.DeleteAtHead() }, []int{}},
{"singleton tail", 1, func(l mutableIntLink) { l.DeleteAtTail() }, []int{}},
{"singleton index", 1, func(l mutableIntLink) { l.DeleteAt(0) }, []int{}},
{"singleton negative index", 1, func(l mutableIntLink) { l.DeleteAt(-1) }, []int{1}},
{"singleton large index", 1, func(l mutableIntLink) { l.DeleteAt(1) }, []int{1}},
{"two item head index", 2, func(l mutableIntLink) { l.DeleteAt(0) }, []int{2}},
{"two item tail index", 2, func(l mutableIntLink) { l.DeleteAt(1) }, []int{1}},
{"middle index", 4, func(l mutableIntLink) { l.DeleteAt(1) }, []int{1, 3, 4}},
{"tail index", 4, func(l mutableIntLink) { l.DeleteAt(3) }, []int{1, 2, 3}},
{"negative index", 4, func(l mutableIntLink) { l.DeleteAt(-1) }, []int{1, 2, 3, 4}},
{"large index", 4, func(l mutableIntLink) { l.DeleteAt(4) }, []int{1, 2, 3, 4}},
} {
t.Run(tt.name, func(t *testing.T) {
defer func() {
if value := recover(); value != nil {
t.Errorf("list mutation panicked: %v", value)
}
}()
l := newLink()
for i := 1; i <= tt.size; i++ {
l.InsertAtTail(i)
}
tt.delete(l)
if got := l.Values(); !reflect.DeepEqual(got, tt.want) {
t.Fatalf("Values() = %v, want %v", got, tt.want)
}
if l.Size() != len(tt.want) || l.IsEmpty() != (len(tt.want) == 0) {
t.Fatalf("Size() = %d, IsEmpty() = %v, want length %d", l.Size(), l.IsEmpty(), len(tt.want))
}
if doubly, ok := l.(*DoublyLink[int]); ok {
current := doubly.Head
for i := 0; current != nil; i++ {
if i >= l.Size() {
t.Fatal("cycle in list after deletion")
}
if current.Next != nil && current.Next.Pre != current {
t.Fatal("deletion did not preserve reciprocal links")
}
current = current.Next
}
}
l.Reverse()
wantReverse := make([]int, len(tt.want))
for i, value := range tt.want {
wantReverse[len(tt.want)-1-i] = value
}
if got := l.Values(); !reflect.DeepEqual(got, wantReverse) {
t.Fatalf("Reverse Values() = %v, want %v", got, wantReverse)
}
l.Reverse()
l.InsertAtHead(9)
if got := l.Values(); !reflect.DeepEqual(got, append([]int{9}, tt.want...)) {
t.Fatalf("reused list Values() = %v", got)
}
})
}
})
}
}
func TestDoublyLinkMiddleMutationBacklinks(t *testing.T) {
l := NewDoublyLink[int]()
l.InsertAtTail(1)
l.InsertAtTail(3)
l.InsertAt(1, 2)
if l.Head.Next.Next.Pre != l.Head.Next {
t.Fatal("middle insertion did not update the next node's Pre link")
}
l.Reverse()
if got := l.Values(); !reflect.DeepEqual(got, []int{3, 2, 1}) {
t.Fatalf("Reverse() after insertion = %v", got)
}
l.DeleteAt(1)
if l.Head.Next.Pre != l.Head {
t.Fatal("middle deletion did not update the next node's Pre link")
}
l.Reverse()
if got := l.Values(); !reflect.DeepEqual(got, []int{1, 3}) {
t.Fatalf("Reverse() after deletion = %v", got)
}
}
+5 -6
View File
@@ -103,6 +103,7 @@ func (sl *SinglyLink[T]) DeleteAtTail() {
current := sl.Head
if current.Next == nil {
sl.DeleteAtHead()
return
}
for current.Next.Next != nil {
@@ -116,22 +117,20 @@ func (sl *SinglyLink[T]) DeleteAtTail() {
// DeleteAt delete value in singly linklist at index
// param `index` should be [0, len(SinglyLink)-1]
func (sl *SinglyLink[T]) DeleteAt(index int) {
if sl.Head == nil {
if sl.Head == nil || index < 0 || index >= sl.length {
return
}
current := sl.Head
if current.Next == nil || index == 0 {
if index == 0 {
sl.DeleteAtHead()
return
}
if index == sl.length-1 {
sl.DeleteAtTail()
}
if index < 0 || index > sl.length-1 {
return
}
current := sl.Head
i := 0
for current != nil {
if i == index-1 {
+1 -1
View File
@@ -354,6 +354,7 @@ func (c *CopyOnWriteList[T]) ForEach(f func(T)) {
func (c *CopyOnWriteList[T]) Sort(compare func(o1 T, o2 T) bool) {
lock := c.lock
lock.Lock()
defer lock.Unlock()
list := c.getList()
sort.Slice(list, func(i, j int) bool {
@@ -374,6 +375,5 @@ func (c *CopyOnWriteList[T]) SubList(start int, end int) (newList []T) {
}
newList = make([]T, end-start)
copy(newList, list[start:end])
c.setList(newList)
return
}
+2 -2
View File
@@ -239,8 +239,8 @@ func (l *List[T]) Merge(other *List[T]) *List[T] {
l1, l2 := len(l.data), len(other.data)
ml := NewList(make([]T, l1+l2))
data := append([]T{}, append(l.data, other.data...)...)
ml.data = data
copy(ml.data, l.data)
copy(ml.data[l1:], other.data)
return ml
}
+18
View File
@@ -0,0 +1,18 @@
package datastructure
import (
"testing"
"github.com/duke-git/lancet/v2/internal"
)
func TestMergePreservesBackingStorage(t *testing.T) {
assert := internal.NewAssert(t, "TestMergePreservesBackingStorage")
backing := []int{1, 99, 100}
left := NewList(backing[:1])
merged := left.Merge(NewList([]int{2, 3}))
assert.Equal([]int{1, 2, 3}, merged.Data())
assert.Equal([]int{1, 99, 100}, backing)
merged.UpdateAt(0, 42)
assert.Equal([]int{1}, left.Data())
}
+69
View File
@@ -0,0 +1,69 @@
package datastructure_test
import (
"reflect"
"testing"
"time"
list "github.com/duke-git/lancet/v2/datastructure/list"
)
func TestCopyOnWriteSortReleasesLock(t *testing.T) {
for _, tc := range []struct {
name string
values []int
panicCompare bool
sort bool
}{
{"sorted values", []int{3, 1, 2}, false, true},
{"empty values", []int{}, false, true},
{"comparator panic", []int{3, 1, 2}, true, true},
{"without sort control", []int{1, 2, 3}, false, false},
} {
t.Run(tc.name, func(t *testing.T) {
c := list.NewCopyOnWriteList(tc.values)
marker := new(int)
if tc.sort {
func() {
defer func() {
r := recover()
if tc.panicCompare {
if r != marker {
t.Errorf("panic identity changed: %v", r)
}
} else if r != nil {
t.Errorf("unexpected panic: %v", r)
}
}()
c.Sort(func(a, b int) bool {
if tc.panicCompare {
panic(marker)
}
return a < b
})
}()
}
if tc.sort && !tc.panicCompare {
got := []int{}
c.ForEach(func(v int) { got = append(got, v) })
want := []int{1, 2, 3}
if len(tc.values) == 0 {
want = []int{}
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("sort got=%v want=%v", got, want)
}
}
done := make(chan struct{})
go func() { c.Add(4); close(done) }()
select {
case <-done:
if c.Size() != len(tc.values)+1 {
t.Fatal("next mutation did not persist")
}
case <-time.After(time.Second):
t.Fatal("Sort left the mutation lock held")
}
})
}
}
@@ -0,0 +1,40 @@
package datastructure_test
import (
list "github.com/duke-git/lancet/v2/datastructure/list"
"reflect"
"testing"
)
func TestCopyOnWriteSubListPreservesReceiver(t *testing.T) {
for _, tc := range []struct {
name string
start, end int
want []int
}{
{"middle", 1, 3, []int{2, 3}}, {"empty range", 2, 2, []int{}}, {"prefix", 0, 2, []int{1, 2}}, {"suffix", 2, 4, []int{3, 4}}, {"full control", 0, 4, []int{1, 2, 3, 4}}, {"invalid control", 3, 1, []int{}},
} {
t.Run(tc.name, func(t *testing.T) {
c := list.NewCopyOnWriteList([]int{1, 2, 3, 4})
got := c.SubList(tc.start, tc.end)
if !reflect.DeepEqual(got, tc.want) {
t.Fatalf("sublist=%v want%v", got, tc.want)
}
values := []int{}
c.ForEach(func(v int) { values = append(values, v) })
if c.Size() != 4 || !reflect.DeepEqual(values, []int{1, 2, 3, 4}) {
t.Fatalf("SubList discarded source values: %v", values)
}
if len(got) > 0 {
got[0] = 99
}
if *c.Get(0) != 1 || *c.Get(1) != 2 {
t.Fatal("returned slice aliases receiver")
}
c.Add(5)
if c.Size() != 5 || *c.Get(4) != 5 {
t.Fatal("later append changed")
}
})
}
}
+2 -2
View File
@@ -136,8 +136,8 @@ func (q *ArrayQueue[T]) Clear() {
// Contain checks if the value is in queue or not
func (q *ArrayQueue[T]) Contain(value T) bool {
for _, v := range q.data {
if reflect.DeepEqual(v, value) {
for i := q.head; i < q.tail; i++ {
if reflect.DeepEqual(q.data[i], value) {
return true
}
}
+4 -5
View File
@@ -57,7 +57,7 @@ func (q *CircularQueue[T]) IsEmpty() bool {
// IsFull checks if queue is full or not
func (q *CircularQueue[T]) IsFull() bool {
return (q.rear+1)%q.capacity == q.front
return q.capacity == 0 || (q.rear+1)%q.capacity == q.front
}
// Front return front value of queue
@@ -101,16 +101,15 @@ func (q *CircularQueue[T]) Dequeue() (*T, error) {
// Clear the queue data
func (q *CircularQueue[T]) Clear() {
q.data = []T{}
q.data = make([]T, q.capacity)
q.front = 0
q.rear = 0
q.capacity = 0
}
// Contain checks if the value is in queue or not
func (q *CircularQueue[T]) Contain(value T) bool {
for _, v := range q.data {
if reflect.DeepEqual(v, value) {
for i := q.front; i != q.rear; i = (i + 1) % q.capacity {
if reflect.DeepEqual(q.data[i], value) {
return true
}
}
+19
View File
@@ -0,0 +1,19 @@
package datastructure
import "testing"
func TestCircularQueueReuseAfterClear(t *testing.T) {
queue := NewCircularQueue[int](3)
queue.Enqueue(1)
queue.Clear()
if err := queue.Enqueue(2); err != nil {
t.Fatal(err)
}
if queue.Size() != 1 || queue.Front() != 2 {
t.Error("cleared queue could not be reused")
}
zero := NewCircularQueue[int](0)
if !zero.IsFull() || zero.Enqueue(1) == nil {
t.Error("zero-capacity queue should reject an element")
}
}
@@ -0,0 +1,25 @@
package datastructure
import "testing"
func TestQueueContainActiveValues(t *testing.T) {
array := NewArrayQueue[int](5)
array.Enqueue(1)
array.Enqueue(2)
array.Dequeue()
if array.Contain(1) || !array.Contain(2) {
t.Error("array queue includes a dequeued value")
}
circular := NewCircularQueue[int](4)
if circular.Contain(0) {
t.Error("empty circular queue contains a zero value")
}
circular.Enqueue(1)
circular.Enqueue(2)
circular.Dequeue()
circular.Enqueue(3)
circular.Enqueue(4)
if circular.Contain(0) || circular.Contain(1) || !circular.Contain(4) {
t.Error("circular queue did not restrict its search to active values")
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ func (t *BSTree[T]) Insert(data T) {
// DeletetNode delete data into BSTree
func (t *BSTree[T]) Delete(data T) {
deleteTreeNode(t.root, data, t.comparator)
t.root = deleteTreeNode(t.root, data, t.comparator)
}
// NodeLevel get node level in BSTree
+66
View File
@@ -0,0 +1,66 @@
package datastructure
import (
"sort"
"strconv"
"testing"
)
func TestBSTreeDeleteReplacesRoot(t *testing.T) {
for i, tt := range []struct {
values []int
remove int
want []int
}{
{nil, 4, nil},
{[]int{2, 1, 3}, 4, []int{1, 2, 3}},
{[]int{6, 5, 7}, 4, []int{5, 6, 7}},
{[]int{2, 6, 1, 3, 5, 7}, 4, []int{1, 2, 3, 5, 6, 7}},
{[]int{2, 6}, 9, []int{2, 4, 6}},
{[]int{2, 6}, 2, []int{4, 6}},
{[]int{2, 6, 1}, 2, []int{1, 4, 6}},
{[]int{2, 6, 1, 3}, 2, []int{1, 3, 4, 6}},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
tree := NewBSTree(4, &intComparator{})
for _, value := range tt.values {
tree.Insert(value)
}
tree.Delete(tt.remove)
checkBSTValues(t, tree, tt.want)
tree.Delete(tt.remove)
checkBSTValues(t, tree, tt.want)
tree.Insert(8)
want := append(append([]int(nil), tt.want...), 8)
sort.Ints(want)
checkBSTValues(t, tree, want)
})
}
}
func checkBSTValues(t *testing.T, tree *BSTree[int], want []int) {
t.Helper()
inOrder := tree.InOrderTraverse()
if !sort.IntsAreSorted(inOrder) {
t.Fatalf("in-order traversal is not sorted: %v", inOrder)
}
for name, got := range map[string][]int{
"in-order": inOrder,
"pre-order": tree.PreOrderTraverse(),
"post-order": tree.PostOrderTraverse(),
"level-order": tree.LevelOrderTraverse(),
} {
sort.Ints(got)
if len(got) != len(want) {
t.Fatalf("%s traversal = %v, want values %v", name, got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("%s traversal = %v, want values %v", name, got, want)
}
}
}
if len(want) == 0 && tree.Depth() != 0 {
t.Fatalf("empty tree depth = %d", tree.Depth())
}
}
+3 -1
View File
@@ -69,6 +69,9 @@ func inOrderTraverse[T any](node *datastructure.TreeNode[T]) []T {
// }
func levelOrderTraverse[T any](root *datastructure.TreeNode[T], traversal *[]T) {
if root == nil {
return
}
var q []*datastructure.TreeNode[T] // queue
var n *datastructure.TreeNode[T] // temp node
@@ -102,7 +105,6 @@ func insertTreeNode[T any](rootNode, newNode *datastructure.TreeNode[T], compara
}
}
// todo, delete root node failed
func deleteTreeNode[T any](node *datastructure.TreeNode[T], data T, comparator constraints.Comparator) *datastructure.TreeNode[T] {
if node == nil {
return nil
+11 -12
View File
@@ -207,7 +207,7 @@ func FormatTimeToStr(t time.Time, format string, timezone ...string) string {
return ""
}
if timezone != nil && timezone[0] != "" {
if len(timezone) > 0 && timezone[0] != "" {
loc, err := time.LoadLocation(timezone[0])
if err != nil {
return ""
@@ -225,7 +225,7 @@ func FormatStrToTime(str, format string, timezone ...string) (time.Time, error)
return time.Time{}, fmt.Errorf("format %s not support", format)
}
if timezone != nil && timezone[0] != "" {
if len(timezone) > 0 && timezone[0] != "" {
loc, err := time.LoadLocation(timezone[0])
if err != nil {
return time.Time{}, err
@@ -343,11 +343,7 @@ func BetweenSeconds(t1 time.Time, t2 time.Time) int64 {
// DayOfYear returns which day of the year the parameter date `t` is.
// Play: https://go.dev/play/p/0hjqhTwFNlH
func DayOfYear(t time.Time) int {
y, m, d := t.Date()
firstDay := time.Date(y, 1, 1, 0, 0, 0, 0, t.Location())
nowDate := time.Date(y, m, d, 0, 0, 0, 0, t.Location())
return int(nowDate.Sub(firstDay).Hours() / 24)
return t.YearDay() - 1
}
// IsWeekend checks if passed time is weekend or not.
@@ -365,7 +361,7 @@ func NowDateOrTime(format string, timezone ...string) string {
return ""
}
if timezone != nil && timezone[0] != "" {
if len(timezone) > 0 && timezone[0] != "" {
loc, err := time.LoadLocation(timezone[0])
if err != nil {
return ""
@@ -382,7 +378,7 @@ func NowDateOrTime(format string, timezone ...string) string {
func Timestamp(timezone ...string) int64 {
t := time.Now()
if timezone != nil && timezone[0] != "" {
if len(timezone) > 0 && timezone[0] != "" {
loc, err := time.LoadLocation(timezone[0])
if err != nil {
return 0
@@ -399,7 +395,7 @@ func Timestamp(timezone ...string) int64 {
func TimestampMilli(timezone ...string) int64 {
t := time.Now()
if timezone != nil && timezone[0] != "" {
if len(timezone) > 0 && timezone[0] != "" {
loc, err := time.LoadLocation(timezone[0])
if err != nil {
return 0
@@ -415,7 +411,7 @@ func TimestampMilli(timezone ...string) int64 {
func TimestampMicro(timezone ...string) int64 {
t := time.Now()
if timezone != nil && timezone[0] != "" {
if len(timezone) > 0 && timezone[0] != "" {
loc, err := time.LoadLocation(timezone[0])
if err != nil {
return 0
@@ -431,7 +427,7 @@ func TimestampMicro(timezone ...string) int64 {
func TimestampNano(timezone ...string) int64 {
t := time.Now()
if timezone != nil && timezone[0] != "" {
if len(timezone) > 0 && timezone[0] != "" {
loc, err := time.LoadLocation(timezone[0])
if err != nil {
return 0
@@ -495,6 +491,9 @@ func GenerateDatetimesBetween(start, end time.Time, layout string, interval stri
if err != nil {
return nil, err
}
if duration <= 0 {
return nil, fmt.Errorf("interval must be positive")
}
for current := start; !current.After(end); current = current.Add(duration) {
result = append(result, current.Format(layout))
+20
View File
@@ -0,0 +1,20 @@
package datetime
import (
"testing"
"time"
_ "time/tzdata"
)
func TestDayOfYearDaylightSaving(t *testing.T) {
loc, err := time.LoadLocation("America/New_York")
if err != nil {
t.Fatal(err)
}
for _, month := range []time.Month{time.January, time.July, time.December} {
date := time.Date(2024, month, 1, 12, 0, 0, 0, loc)
if got, want := DayOfYear(date), date.YearDay()-1; got != want {
t.Errorf("%v: got %d, want %d", date, got, want)
}
}
}
+17
View File
@@ -0,0 +1,17 @@
package datetime
import (
"testing"
"time"
)
func TestGenerateDatetimesNonPositiveInterval(t *testing.T) {
start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
for _, interval := range []string{"0s", "-1h"} {
t.Run(interval, func(t *testing.T) {
if _, err := GenerateDatetimesBetween(start, start.Add(time.Hour), "2006-01-02", interval); err == nil {
t.Error("expected an error for a non-positive interval")
}
})
}
}
+25
View File
@@ -0,0 +1,25 @@
package datetime
import (
"testing"
"time"
)
func TestEmptyTimezoneOptions(t *testing.T) {
empty := []string{}
now := time.Now()
if got := FormatTimeToStr(now, "yyyy-mm-dd", empty...); got != FormatTimeToStr(now, "yyyy-mm-dd") {
t.Errorf("unexpected date: %s", got)
}
if _, err := FormatStrToTime("2026-01-02", "yyyy-mm-dd", empty...); err != nil {
t.Fatal(err)
}
if NowDateOrTime("yyyy", empty...) == "" {
t.Error("empty date")
}
for _, fn := range []func(...string) int64{Timestamp, TimestampMilli, TimestampMicro, TimestampNano} {
if fn(empty...) == 0 {
t.Error("empty timestamp")
}
}
}
+1 -1
View File
@@ -276,7 +276,7 @@ func main() {
func UpperCamelCase(s string) string
```
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/todo)</span></b>
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/65Q6iYhLRTa)</span></b>
```go
import (
+1 -1
View File
@@ -276,7 +276,7 @@ func main() {
func UpperCamelCase(s string) string
```
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/todo)</span></b>
<b>Example:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/vPHszjTXSFp)</span></b>
```go
import (
+9 -20
View File
@@ -229,6 +229,13 @@ func CopyFile(srcPath string, dstPath string) error {
return err
}
defer srcFile.Close()
srcInfo, err := srcFile.Stat()
if err != nil {
return err
}
if dstInfo, err := os.Stat(dstPath); err == nil && os.SameFile(srcInfo, dstInfo) {
return errors.New("source and destination are the same file")
}
distFile, err := os.Create(dstPath)
if err != nil {
@@ -754,6 +761,8 @@ func WriteCsvFile(filepath string, records [][]string, append bool, delimiter ..
if append {
flag = flag | os.O_APPEND
} else {
flag = flag | os.O_TRUNC
}
f, err := os.OpenFile(filepath, flag, 0644)
@@ -771,13 +780,6 @@ func WriteCsvFile(filepath string, records [][]string, append bool, delimiter ..
writer.Comma = ','
}
// 遍历所有记录并处理包含分隔符或双引号的单元格
for i := range records {
for j := range records[i] {
records[i][j] = escapeCSVField(records[i][j], writer.Comma)
}
}
return writer.WriteAll(records)
}
@@ -836,19 +838,6 @@ func ReadFile(path string) (reader io.ReadCloser, closeFn func(), err error) {
}
}
// escapeCSVField 处理单元格内容,如果包含分隔符,则用双引号包裹
func escapeCSVField(field string, delimiter rune) string {
// 替换所有的双引号为两个双引号
escapedField := strings.ReplaceAll(field, "\"", "\"\"")
// 如果字段包含分隔符、双引号或换行符,用双引号包裹整个字段
if strings.ContainsAny(escapedField, string(delimiter)+"\"\n") {
escapedField = fmt.Sprintf("\"%s\"", escapedField)
}
return escapedField
}
// WriteMapsToCsv write slice of map to csv file.
// Play: https://go.dev/play/p/umAIomZFV1c
// filepath: Path to the CSV file.
+8 -1
View File
@@ -4,6 +4,8 @@ import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"runtime"
"sync"
@@ -430,7 +432,12 @@ func ExampleWriteBytesToFile() {
}
func ExampleReadFile() {
reader, fn, err := ReadFile("https://httpbin.org/robots.txt")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "User-agent: *\nDisallow: /deny\n")
}))
defer ts.Close()
reader, fn, err := ReadFile(ts.URL)
if err != nil {
return
}
+68 -2
View File
@@ -1,7 +1,10 @@
package fileutil
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
@@ -136,6 +139,34 @@ func TestCopyFile(t *testing.T) {
os.Remove(destFile)
}
func TestCopyFilePreservesSource(t *testing.T) {
for _, mode := range []string{"same", "hardlink", "different"} {
t.Run(mode, func(t *testing.T) {
assert := internal.NewAssert(t, "TestCopyFilePreservesSource")
src := filepath.Join(t.TempDir(), "source.txt")
assert.IsNil(os.WriteFile(src, []byte("keep this content"), 0600))
dst := src + ".copy"
if mode == "same" {
dst = src
} else if mode == "hardlink" {
if err := os.Link(src, dst); err != nil {
t.Skipf("hard links unavailable: %v", err)
}
}
err := CopyFile(src, dst)
if (err != nil) != (mode != "different") {
t.Errorf("unexpected copy result: %v", err)
}
data, err := os.ReadFile(src)
assert.IsNil(err)
assert.Equal("keep this content", string(data))
data, err = os.ReadFile(dst)
assert.IsNil(err)
assert.Equal("keep this content", string(data))
})
}
}
func TestReadFileToString(t *testing.T) {
assert := internal.NewAssert(t, "TestReadFileToString")
@@ -415,6 +446,36 @@ func TestWriteCsvFile(t *testing.T) {
// RemoveFile(csvFilePath)
}
func TestWriteCsvFileRoundTrip(t *testing.T) {
for _, delimiter := range []rune{',', ';'} {
t.Run(string(delimiter), func(t *testing.T) {
assert := internal.NewAssert(t, "TestWriteCsvFileRoundTrip")
path := filepath.Join(t.TempDir(), "quoted.csv")
want := [][]string{{"a,b;c", "\"quoted\"", "two\nlines"}}
records := [][]string{append([]string(nil), want[0]...)}
assert.IsNil(WriteCsvFile(path, records, false, delimiter))
got, err := ReadCsvFile(path, delimiter)
assert.IsNil(err)
assert.Equal(want, got)
assert.Equal(want, records)
})
}
}
func TestWriteCsvFileOverwriteAndAppend(t *testing.T) {
assert := internal.NewAssert(t, "TestWriteCsvFileOverwriteAndAppend")
path := filepath.Join(t.TempDir(), "overwrite.csv")
assert.IsNil(os.WriteFile(path, []byte(strings.Repeat("old,value\n", 10)), 0600))
assert.IsNil(WriteCsvFile(path, [][]string{{"new", "row"}}, false))
got, err := ReadCsvFile(path)
assert.IsNil(err)
assert.Equal([][]string{{"new", "row"}}, got)
assert.IsNil(WriteCsvFile(path, [][]string{{"appended", "row"}}, true))
got, err = ReadCsvFile(path)
assert.IsNil(err)
assert.Equal([][]string{{"new", "row"}, {"appended", "row"}}, got)
}
func TestWriteMapsToCsv(t *testing.T) {
assert := internal.NewAssert(t, "TestWriteMapsToCSV")
@@ -516,9 +577,14 @@ func TestWriteBytesToFile(t *testing.T) {
}
func TestReadFile(t *testing.T) {
reader, close, err := ReadFile("https://httpbin.org/robots.txt")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "User-agent: *\nDisallow: /deny\n")
}))
defer ts.Close()
reader, close, err := ReadFile(ts.URL)
if err != nil {
t.Fail()
t.Fatal(err)
}
defer close()
+4 -7
View File
@@ -26,8 +26,9 @@ func Comma[T constraints.Float | constraints.Integer | string](value T, prefixSy
return ""
}
isNegative := strings.HasPrefix(numString, "-")
if isNegative {
sign := ""
if strings.HasPrefix(numString, "-") || strings.HasPrefix(numString, "+") {
sign = numString[:1]
numString = numString[1:]
}
@@ -41,11 +42,7 @@ func Comma[T constraints.Float | constraints.Integer | string](value T, prefixSy
numString = numString[:index] + "," + numString[index:]
}
if isNegative {
numString = "-" + numString
}
return prefixSymbol + numString
return prefixSymbol + sign + numString
}
// Pretty data to JSON string.
+3
View File
@@ -11,6 +11,9 @@ func TestComma(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestComma")
assert.Equal("+123", Comma("+123", ""))
assert.Equal("+123,456", Comma("+123456", ""))
assert.Equal("$+123,456.78", Comma("+123456.78", "$"))
assert.Equal("", Comma("", ""))
assert.Equal("", Comma("aa", ""))
+26
View File
@@ -0,0 +1,26 @@
package iterator
import "testing"
func TestTakeHasNextExhaustedSource(t *testing.T) {
it := Take[int](FromSlice([]int{1}), 3)
if value, ok := it.Next(); !ok || value != 1 {
t.Fatal("missing first value")
}
if it.HasNext() {
t.Error("exhausted source still reports an element")
}
}
func TestFilterHasNextMatches(t *testing.T) {
it := Filter[int](FromSlice([]int{1, 2, 3}), func(value int) bool { return value%2 == 0 })
if !it.HasNext() || !it.HasNext() {
t.Fatal("missing matching value")
}
if value, ok := it.Next(); !ok || value != 2 {
t.Fatal("HasNext consumed the matching value")
}
if it.HasNext() {
t.Error("unmatched tail reported as another element")
}
}
+13 -2
View File
@@ -44,9 +44,17 @@ func Filter[T any](iter Iterator[T], predicateFunc func(item T) bool) Iterator[T
type filterIterator[T any] struct {
iter Iterator[T]
predicateFunc func(T) bool
next T
ready bool
}
func (fr *filterIterator[T]) Next() (T, bool) {
if fr.ready {
item := fr.next
var zero T
fr.next, fr.ready = zero, false
return item, true
}
for item, ok := fr.iter.Next(); ok; item, ok = fr.iter.Next() {
if fr.predicateFunc(item) {
return item, true
@@ -57,7 +65,10 @@ func (fr *filterIterator[T]) Next() (T, bool) {
}
func (fr *filterIterator[T]) HasNext() bool {
return fr.iter.HasNext()
if !fr.ready {
fr.next, fr.ready = fr.Next()
}
return fr.ready
}
// Join creates an iterator that join all elements of iters[0], then all elements of iters[1] and so on.
@@ -135,5 +146,5 @@ func (iter *takeIterator[T]) Next() (T, bool) {
}
func (iter *takeIterator[T]) HasNext() bool {
return iter.num > 0
return iter.num > 0 && iter.it.HasNext()
}
+13 -3
View File
@@ -318,6 +318,10 @@ func HasKey[K comparable, V any](m map[K]V, key K) bool {
// MapToStruct converts map to struct
// Play: https://go.dev/play/p/7wYyVfX38Dp
func MapToStruct(m map[string]any, structObj any) error {
target := reflect.ValueOf(structObj)
if target.Kind() != reflect.Ptr || target.IsNil() || target.Elem().Kind() != reflect.Struct {
return fmt.Errorf("destination must be a non-nil pointer to a struct")
}
for k, v := range m {
err := setStructField(structObj, k, v)
if err != nil {
@@ -347,6 +351,9 @@ func setStructField(structObj any, fieldName string, fieldValue any) error {
}
val := reflect.ValueOf(fieldValue)
if !val.IsValid() {
return fmt.Errorf("Map value type don't match struct field type")
}
if fieldVal.Type() != val.Type() {
@@ -575,9 +582,12 @@ func convertSlice(src reflect.Value, dst reflect.Value) error {
return fmt.Errorf("error type:%s", dst.Type().String())
}
l := src.Len()
target := reflect.MakeSlice(dst.Type(), l, l)
if dst.CanSet() {
dst.Set(target)
if dst.Kind() == reflect.Array {
if l > dst.Len() {
return fmt.Errorf("source length %d exceeds destination length %d", l, dst.Len())
}
} else if dst.CanSet() {
dst.Set(reflect.MakeSlice(dst.Type(), l, l))
}
for i := 0; i < l; i++ {
srcValue := src.Index(i)
+27
View File
@@ -11,6 +11,33 @@ import (
"github.com/duke-git/lancet/v2/internal"
)
func TestMapToArray(t *testing.T) {
var dst [2]int
if err := MapTo([]int{3, 4}, &dst); err != nil || dst != [2]int{3, 4} {
t.Fatalf("got %v, %v", dst, err)
}
if err := MapTo([]int{1, 2, 3}, &dst); err == nil {
t.Fatal("expected an error for an oversized source")
}
}
func TestMapToStructInvalidInputs(t *testing.T) {
var nilTarget *struct {
Name string `json:"name"`
}
for _, dst := range []any{nil, nilTarget, 1, &[]int{}} {
if err := MapToStruct(map[string]any{"name": "value"}, dst); err == nil {
t.Errorf("expected an error for %T", dst)
}
}
var dst struct {
Name string `json:"name"`
}
if err := MapToStruct(map[string]any{"name": nil}, &dst); err == nil {
t.Error("expected an error for an incompatible nil value")
}
}
func TestKeys(t *testing.T) {
t.Parallel()
+25 -27
View File
@@ -35,8 +35,10 @@ func Fibonacci(first, second, n int) int {
if n <= 0 {
return 0
}
if n < 3 {
return 1
if n == 1 {
return first
} else if n == 2 {
return second
} else if n == 3 {
return first + second
} else {
@@ -75,8 +77,7 @@ func Percent(val, total float64, n int) float64 {
// Play: https://go.dev/play/p/kZwpBRAcllO
func RoundToString[T constraints.Float | constraints.Integer](x T, n int) string {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Round(float64(x))
r := math.Round(float64(x) * tmp)
result := strconv.FormatFloat(r/tmp, 'f', n, 64)
return result
}
@@ -85,15 +86,14 @@ func RoundToString[T constraints.Float | constraints.Integer](x T, n int) string
// Play: https://go.dev/play/p/ghyb528JRJL
func RoundToFloat[T constraints.Float | constraints.Integer](x T, n int) float64 {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Round(float64(x))
r := math.Round(float64(x) * tmp)
return r / tmp
}
// TruncRound round off n decimal places.
// Play: https://go.dev/play/p/aumarSHIGzP
func TruncRound[T constraints.Float | constraints.Integer](x T, n int) T {
floatStr := fmt.Sprintf("%."+strconv.Itoa(n+1)+"f", x)
floatStr := fmt.Sprintf("%."+strconv.Itoa(n+1)+"f", float64(x))
temp := strings.Split(floatStr, ".")
var newFloat string
if len(temp) < 2 || n >= len(temp[1]) {
@@ -109,8 +109,7 @@ func TruncRound[T constraints.Float | constraints.Integer](x T, n int) T {
// Play: https://go.dev/play/p/vbCBrQHZEED
func FloorToFloat[T constraints.Float | constraints.Integer](x T, n int) float64 {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Floor(float64(x))
r := math.Floor(float64(x) * tmp)
return r / tmp
}
@@ -118,8 +117,7 @@ func FloorToFloat[T constraints.Float | constraints.Integer](x T, n int) float64
// Play: https://go.dev/play/p/Qk9KPd2IdDb
func FloorToString[T constraints.Float | constraints.Integer](x T, n int) string {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Floor(float64(x))
r := math.Floor(float64(x) * tmp)
result := strconv.FormatFloat(r/tmp, 'f', n, 64)
return result
}
@@ -128,8 +126,7 @@ func FloorToString[T constraints.Float | constraints.Integer](x T, n int) string
// Play: https://go.dev/play/p/8hOeSADZPCo
func CeilToFloat[T constraints.Float | constraints.Integer](x T, n int) float64 {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Ceil(float64(x))
r := math.Ceil(float64(x) * tmp)
return r / tmp
}
@@ -137,8 +134,7 @@ func CeilToFloat[T constraints.Float | constraints.Integer](x T, n int) float64
// Play: https://go.dev/play/p/wy5bYEyUKKG
func CeilToString[T constraints.Float | constraints.Integer](x T, n int) string {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Ceil(float64(x))
r := math.Ceil(float64(x) * tmp)
result := strconv.FormatFloat(r/tmp, 'f', n, 64)
return result
}
@@ -270,12 +266,17 @@ func Range[T constraints.Integer | constraints.Float](start T, count int) []T {
func RangeWithStep[T constraints.Integer | constraints.Float](start, end, step T) []T {
result := []T{}
if start >= end || step == 0 {
if start >= end || step <= 0 {
return result
}
for i := start; i < end; i += step {
for i := start; i < end; {
result = append(result, i)
next := i + step
if next <= i {
break
}
i = next
}
return result
@@ -298,11 +299,7 @@ func RadianToAngle(radian float64) float64 {
// PointDistance get two points distance.
// Play: https://go.dev/play/p/RrG4JIaziM8
func PointDistance(x1, y1, x2, y2 float64) float64 {
a := x1 - x2
b := y1 - y2
c := math.Pow(a, 2) + math.Pow(b, 2)
return math.Sqrt(c)
return math.Hypot(x1-x2, y1-y2)
}
// IsPrime checks if number is prime number.
@@ -363,7 +360,7 @@ func lcm[T constraints.Integer](a, b T) T {
if a == 0 || b == 0 {
panic("lcm function: provide non zero integers only.")
}
return a * b / gcd(a, b)
return a / gcd(a, b) * b
}
// Cos returns the cosine of the radian argument.
@@ -441,10 +438,11 @@ func Permutation(n, k uint) uint {
return 0
}
nFactorial := Factorial(n)
nMinusKFactorial := Factorial(n - k)
return nFactorial / nMinusKFactorial
result := uint(1)
for i := uint(0); i < k; i++ {
result *= n - i
}
return result
}
// Combination calculate C(n, k).
+43
View File
@@ -17,6 +17,19 @@ func TestExponent(t *testing.T) {
assert.Equal(int64(100), Exponent(10, 2))
}
func TestTruncRoundInteger(t *testing.T) {
assert := internal.NewAssert(t, "TestTruncRoundInteger")
assert.Equal(42, TruncRound(42, 0))
assert.Equal(int8(-42), TruncRound(int8(-42), 2))
}
func TestLCMIntermediateOverflow(t *testing.T) {
assert := internal.NewAssert(t, "TestLCMIntermediateOverflow")
assert.Equal(uint8(32), LCM(uint8(32)))
assert.Equal(uint8(192), LCM(uint8(64), uint8(96)))
assert.Equal(uint64(1<<40), LCM(uint64(1<<40), uint64(1<<39)))
}
func TestFibonacci(t *testing.T) {
t.Parallel()
@@ -26,6 +39,10 @@ func TestFibonacci(t *testing.T) {
assert.Equal(1, Fibonacci(1, 1, 1))
assert.Equal(1, Fibonacci(1, 1, 2))
assert.Equal(5, Fibonacci(1, 1, 5))
assert.Equal(2, Fibonacci(2, 3, 1))
assert.Equal(3, Fibonacci(2, 3, 2))
assert.Equal(5, Fibonacci(2, 3, 3))
assert.Equal(13, Fibonacci(2, 3, 5))
}
func TestFactorial(t *testing.T) {
@@ -49,6 +66,19 @@ func TestPercent(t *testing.T) {
assert.EqualValues(-7.42, Percent(-30305, 408420, 2))
}
func TestIntegerRoundingScale(t *testing.T) {
assert := internal.NewAssert(t, "TestIntegerRoundingScale")
assert.Equal(float64(42), RoundToFloat(int8(42), 2))
assert.Equal("42.00", RoundToString(int8(42), 2))
assert.Equal(float64(42), FloorToFloat(uint8(42), 2))
assert.Equal("42.00", FloorToString(uint8(42), 2))
assert.Equal(float64(42), CeilToFloat(int8(42), 2))
assert.Equal("42.00", CeilToString(int8(42), 2))
assert.Equal(float64(150), RoundToFloat(149, -1))
assert.Equal(float64(140), FloorToFloat(149, -1))
assert.Equal(float64(150), CeilToFloat(149, -1))
}
func TestRoundToFloat(t *testing.T) {
t.Parallel()
@@ -260,6 +290,10 @@ func TestRangeWithStep(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestRangeWithStep")
assert.Equal([]int8{120}, RangeWithStep[int8](120, 125, 10))
assert.Equal([]uint8{250}, RangeWithStep[uint8](250, 253, 10))
assert.Equal([]int{}, RangeWithStep(1, 4, -1))
assert.Equal([]float64{1e20}, RangeWithStep(1e20, 1e21, 1.0))
result1 := RangeWithStep(1, 4, 1)
result2 := RangeWithStep(1, -1, 0)
@@ -300,6 +334,15 @@ func TestRadianToAngle(t *testing.T) {
assert.Equal(float64(45), result3)
}
func TestPointDistanceExtremeScales(t *testing.T) {
for _, scale := range []float64{1e200, 1e-200} {
got := PointDistance(0, 0, 3*scale, 4*scale)
if math.Abs(got/(5*scale)-1) > 1e-15 {
t.Errorf("scale %g: got %g, want %g", scale, got, 5*scale)
}
}
}
func TestPointDistance(t *testing.T) {
t.Parallel()
+40
View File
@@ -0,0 +1,40 @@
package netutil
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
func TestDownloadFileStatus(t *testing.T) {
for _, status := range []int{http.StatusOK, http.StatusNotFound, http.StatusInternalServerError} {
t.Run(http.StatusText(status), func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status)
w.Write([]byte("response"))
}))
defer server.Close()
path := filepath.Join(t.TempDir(), "download.txt")
if err := os.WriteFile(path, []byte("original"), 0600); err != nil {
t.Fatal(err)
}
err := DownloadFile(path, server.URL)
if (err != nil) != (status != http.StatusOK) {
t.Errorf("unexpected result: %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
want := "original"
if status == http.StatusOK {
want = "response"
}
if string(data) != want {
t.Errorf("got %q, want %q", data, want)
}
})
}
}
+28 -8
View File
@@ -412,21 +412,41 @@ func TestHttpClient_AsyncSendRequest(t *testing.T) {
}
func TestProxy(t *testing.T) {
t.Parallel()
// Local target server
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}))
defer target.Close()
// Local proxy server (basic forwarding stub)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// In real proxy tests, you'd validate request form and forward.
// Keep deterministic: just return OK.
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("proxied"))
}))
defer proxy.Close()
proxyURL, err := url.Parse(proxy.URL)
if err != nil {
t.Fatalf("failed to parse proxy URL: %v", err)
}
config := &HttpClientConfig{
HandshakeTimeout: 20 * time.Second,
ResponseTimeout: 40 * time.Second,
// Use the proxy ip to add it here
//Proxy: &url.URL{
// Scheme: "http",
// Host: "46.17.63.166:18888",
//},
Proxy: proxyURL,
}
httpClient := NewHttpClientWithConfig(config)
resp, err := httpClient.Get("https://www.ipplus360.com/getLocation")
resp, err := httpClient.Get(target.URL)
if err != nil {
t.Log("net error: ", err.Error())
return
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected %d, got %d", http.StatusOK, resp.StatusCode)
+35 -9
View File
@@ -70,12 +70,12 @@ func GetInternalIp() string {
func GetRequestPublicIp(req *http.Request) string {
var ip string
for _, ip = range strings.Split(req.Header.Get("X-Forwarded-For"), ",") {
if ip = strings.TrimSpace(ip); ip != "" && !IsInternalIP(net.ParseIP(ip)) {
if ip = strings.TrimSpace(ip); net.ParseIP(ip) != nil && !IsInternalIP(net.ParseIP(ip)) {
return ip
}
}
if ip = strings.TrimSpace(req.Header.Get("X-Real-Ip")); ip != "" && !IsInternalIP(net.ParseIP(ip)) {
if ip = strings.TrimSpace(req.Header.Get("X-Real-Ip")); net.ParseIP(ip) != nil && !IsInternalIP(net.ParseIP(ip)) {
return ip
}
@@ -192,18 +192,37 @@ func IsPublicIP(IP net.IP) bool {
// IsInternalIP verify an ip is intranet or not.
// Play: https://go.dev/play/p/sYGhXbgO4Cb
func IsInternalIP(IP net.IP) bool {
if IP.IsLoopback() {
if IP.IsLoopback() || IP.IsPrivate() || IP.IsLinkLocalUnicast() {
return true
}
if ip4 := IP.To4(); ip4 != nil {
return ip4[0] == 10 ||
(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) ||
(ip4[0] == 169 && ip4[1] == 254) ||
(ip4[0] == 192 && ip4[1] == 168)
// RFC 6598 shared address space, 100.64.0.0/10.
return ip4[0] == 100 && ip4[1]&0xc0 == 0x40
}
if embedded, ok := embeddedTransitionIPv4(IP); ok {
return IsInternalIP(embedded)
}
return false
}
// embeddedTransitionIPv4 returns the IPv4 address carried by a NAT64, 6to4, or Teredo address.
func embeddedTransitionIPv4(ip net.IP) (net.IP, bool) {
ip = ip.To16()
if ip == nil || ip.To4() != nil {
return nil, false
}
switch {
case bytes.Equal(ip[:12], []byte{0x00, 0x64, 0xff, 0x9b, 0, 0, 0, 0, 0, 0, 0, 0}):
return net.IPv4(ip[12], ip[13], ip[14], ip[15]), true
case ip[0] == 0x20 && ip[1] == 0x02:
return net.IPv4(ip[2], ip[3], ip[4], ip[5]), true
case ip[0] == 0x20 && ip[1] == 0x01 && ip[2] == 0 && ip[3] == 0:
return net.IPv4(ip[12]^0xff, ip[13]^0xff, ip[14]^0xff, ip[15]^0xff), true
default:
return nil, false
}
}
// EncodeUrl encode url.
// Play: https://go.dev/play/p/bsZ6BRC4uKI
func EncodeUrl(urlStr string) (string, error) {
@@ -250,10 +269,14 @@ func UploadFile(filepath string, server string) (bool, error) {
contentType := writer.FormDataContentType()
writer.Close()
_, err = http.Post(server, contentType, bodyBuffer)
resp, err := http.Post(server, contentType, bodyBuffer)
if err != nil {
return false, err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return false, fmt.Errorf("upload failed: %s", resp.Status)
}
return true, nil
}
@@ -265,6 +288,9 @@ func DownloadFile(filepath string, url string) error {
return err
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return fmt.Errorf("download failed: %s", resp.Status)
}
out, err := os.Create(filepath)
if err != nil {
@@ -296,7 +322,7 @@ func IsPingConnected(host string) bool {
// IsTelnetConnected checks if can telnet specified host or not.
// Play: https://go.dev/play/p/yiLCGtQv_ZG
func IsTelnetConnected(host string, port string) bool {
adder := host + ":" + port
adder := net.JoinHostPort(host, port)
conn, err := net.DialTimeout("tcp", adder, 5*time.Second)
if err != nil {
+30
View File
@@ -95,6 +95,36 @@ func TestIsInternalIP(t *testing.T) {
}
}
func TestIsInternalIPCoversIPv6AndSharedSpace(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestIsInternalIPCoversIPv6AndSharedSpace")
ips := []net.IP{
net.ParseIP("fd12:3456:789a::1"),
net.ParseIP("fe80::1"),
net.ParseIP("100.64.0.1"),
net.ParseIP("100.127.255.255"),
net.ParseIP("100.63.255.255"),
net.ParseIP("100.128.0.1"),
net.ParseIP("::ffff:192.168.0.1"),
net.ParseIP("64:ff9b::a9fe:a9fe"),
net.ParseIP("64:ff9b::808:808"),
net.ParseIP("2002:7f00:1::1"),
net.ParseIP("2002:808:808::1"),
net.ParseIP("2001::80ff:fffe"),
net.ParseIP("2001:db8::1"),
}
expected := []bool{
true, true, true, true, false, false, true,
true, false, true, false, true, false,
}
for i := 0; i < len(ips); i++ {
assert.Equal(expected[i], IsInternalIP(ips[i]))
}
}
func TestGetIps(t *testing.T) {
ips := GetIps()
t.Log(ips)
+22
View File
@@ -0,0 +1,22 @@
package netutil
import (
"net/http/httptest"
"testing"
)
func TestGetRequestPublicIpInvalidHeaders(t *testing.T) {
for _, test := range []struct{ forwarded, real, want string }{
{"unknown, 8.8.8.8", "", "8.8.8.8"},
{"unknown", "invalid", "1.1.1.1"},
{"10.0.0.1", "8.8.4.4", "8.8.4.4"},
} {
req := httptest.NewRequest("GET", "http://example.com", nil)
req.RemoteAddr = "1.1.1.1:1234"
req.Header.Set("X-Forwarded-For", test.forwarded)
req.Header.Set("X-Real-Ip", test.real)
if got := GetRequestPublicIp(req); got != test.want {
t.Errorf("got %q, want %q", got, test.want)
}
}
}
+21
View File
@@ -0,0 +1,21 @@
package netutil
import (
"net"
"testing"
)
func TestIsTelnetConnectedIPv6(t *testing.T) {
listener, err := net.Listen("tcp6", "[::1]:0")
if err != nil {
t.Skipf("IPv6 loopback unavailable: %v", err)
}
defer listener.Close()
host, port, err := net.SplitHostPort(listener.Addr().String())
if err != nil {
t.Fatal(err)
}
if !IsTelnetConnected(host, port) {
t.Error("could not connect to the IPv6 loopback listener")
}
}
+41
View File
@@ -0,0 +1,41 @@
package netutil
import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
)
type uploadResponseBody struct {
io.Reader
closed bool
}
func (b *uploadResponseBody) Close() error { b.closed = true; return nil }
type uploadTransport struct{ response *http.Response }
func (t uploadTransport) RoundTrip(*http.Request) (*http.Response, error) { return t.response, nil }
func TestUploadFileResponse(t *testing.T) {
path := filepath.Join(t.TempDir(), "upload.txt")
if err := os.WriteFile(path, []byte("content"), 0600); err != nil {
t.Fatal(err)
}
original := http.DefaultClient
defer func() { http.DefaultClient = original }()
for _, status := range []int{http.StatusCreated, http.StatusInternalServerError} {
body := &uploadResponseBody{Reader: strings.NewReader("response")}
http.DefaultClient = &http.Client{Transport: uploadTransport{&http.Response{StatusCode: status, Body: body}}}
ok, err := UploadFile(path, "https://example.com/upload")
if ok != (status == http.StatusCreated) || (err != nil) != (status != http.StatusCreated) {
t.Errorf("status %d: got %v, %v", status, ok, err)
}
if !body.closed {
t.Errorf("status %d: response body was not closed", status)
}
}
}
+14
View File
@@ -0,0 +1,14 @@
package random
import "testing"
func TestUniqueRandomSlicesNonPositiveLength(t *testing.T) {
for _, length := range []int{-1, 0} {
if got := RandUniqueIntSlice(length, 1, 10); len(got) != 0 {
t.Errorf("unexpected integers: %v", got)
}
if got := RandFloats(length, 1, 10, 2); len(got) != 0 {
t.Errorf("unexpected floats: %v", got)
}
}
}
+4 -1
View File
@@ -98,7 +98,7 @@ func RandIntSlice(length, min, max int) []int {
// RandUniqueIntSlice generate a slice of random int of length that do not repeat.
// Play: https://go.dev/play/p/uBkRSOz73Ec
func RandUniqueIntSlice(length, min, max int) []int {
if min > max {
if length <= 0 || min > max {
return []int{}
}
if length > max-min {
@@ -139,6 +139,9 @@ func RandFloat(min, max float64, precision int) float64 {
// RandFloats generate a slice of random float64 numbers of length that do not repeat.
// Play: https://go.dev/play/p/I3yndUQ-rhh
func RandFloats(length int, min, max float64, precision int) []float64 {
if length <= 0 {
return []float64{}
}
if max < min {
min, max = max, min
}
+11
View File
@@ -0,0 +1,11 @@
package slice
import "testing"
func TestUniqueByFieldUnreadableInputs(t *testing.T) {
for _, input := range []any{nil, struct{ hidden string }{"value"}} {
if _, err := UniqueByField([]any{input}, "hidden"); err == nil {
t.Errorf("expected an error for %T", input)
}
}
}
+12 -5
View File
@@ -187,8 +187,8 @@ func DifferenceBy[T comparable](slice []T, comparedSlice []T, iteratee func(inde
result := make([]T, 0)
comparedMap := make(map[T]struct{}, len(comparedSlice))
for _, item := range comparedSlice {
comparedMap[iteratee(0, item)] = struct{}{}
for i, item := range comparedSlice {
comparedMap[iteratee(i, item)] = struct{}{}
}
for i, item := range slice {
@@ -712,6 +712,9 @@ func IntSlice(slice any) []int {
// Play: https://go.dev/play/p/800B1dPBYyd
func DeleteAt[T any](slice []T, index int) []T {
result := append([]T(nil), slice...)
if len(result) == 0 {
return result
}
if index < 0 || index >= len(slice) {
return result[:len(slice)-1]
@@ -949,10 +952,8 @@ func UniqueByField[T any](slice []T, field string) ([]T, error) {
func getField[T any](item T, field string) (interface{}, error) {
v := reflect.ValueOf(item)
t := reflect.TypeOf(item)
if t.Kind() == reflect.Ptr {
t = t.Elem()
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
@@ -963,6 +964,9 @@ func getField[T any](item T, field string) (interface{}, error) {
if !f.IsValid() {
return nil, fmt.Errorf("field name %s not found", field)
}
if !f.CanInterface() {
return nil, fmt.Errorf("field name %s is not exported", field)
}
return v.FieldByName(field).Interface(), nil
}
@@ -1015,6 +1019,9 @@ func Merge[T any](slices ...[]T) []T {
// Play: https://go.dev/play/p/anJXfB5wq_t
func Intersection[T comparable](slices ...[]T) []T {
result := []T{}
if len(slices) == 0 {
return result
}
elementCount := make(map[T]int)
for _, slice := range slices {
+6
View File
@@ -801,6 +801,8 @@ func TestDeleteAt(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestDeleteAt")
assert.Equal(0, len(DeleteAt([]int(nil), 0)))
assert.Equal(0, len(DeleteAt([]int{}, -1)))
tests := []struct {
slice []int
@@ -1103,6 +1105,7 @@ func TestIntersection(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestIntersection")
assert.Equal([]int{}, Intersection[int]())
s1 := []int{1, 2, 2, 3}
s2 := []int{1, 2, 3, 4}
@@ -1203,6 +1206,9 @@ func TestDifferenceBy(t *testing.T) {
}
assert.Equal([]int{1, 2}, DifferenceBy(s1, s2, addOne))
assert.Equal([]int{}, DifferenceBy([]int{10, 10}, []int{10, 10}, func(i, v int) int {
return i + v
}))
}
func TestIsAscending(t *testing.T) {
+13
View File
@@ -0,0 +1,13 @@
package stream
import (
"math"
"testing"
)
func TestLimitLargerThanSource(t *testing.T) {
got := Of(1, 2).Limit(math.MaxInt).ToSlice()
if len(got) != 2 || got[0] != 1 || got[1] != 2 {
t.Errorf("unexpected limited stream: %v", got)
}
}
+13
View File
@@ -0,0 +1,13 @@
package stream
import "testing"
func TestFromRangeSmallIntegerSpan(t *testing.T) {
got := FromRange[int8](-120, 120, 10).ToSlice()
if len(got) != 25 || got[0] != -120 || got[len(got)-1] != 120 {
t.Errorf("unexpected range: %v", got)
}
if got := FromRange[uint8](250, 255, 5).ToSlice(); len(got) != 2 || got[1] != 255 {
t.Errorf("unexpected unsigned range: %v", got)
}
}
+11 -5
View File
@@ -101,11 +101,14 @@ func FromRange[T constraints.Integer | constraints.Float](start, end, step T) St
panic("stream.FromRange: param step should be positive")
}
l := int((end-start)/step) + 1
source := make([]T, l)
for i := 0; i < l; i++ {
source[i] = start + (T(i) * step)
source := make([]T, 0)
for value := start; value <= end; {
source = append(source, value)
next := value + step
if next <= value {
break
}
value = next
}
return FromSlice(source)
@@ -219,6 +222,9 @@ func (s Stream[T]) Limit(maxSize int) Stream[T] {
if maxSize < 0 {
return FromSlice([]T{})
}
if maxSize > len(s.source) {
maxSize = len(s.source)
}
source := make([]T, 0, maxSize)
+23
View File
@@ -0,0 +1,23 @@
package structs
import "testing"
func TestFieldIsEmbeddedUsesAnonymousFlag(t *testing.T) {
type Embedded struct{ Promoted int }
s := New(struct {
Embedded
Named Embedded
}{})
for _, tt := range []struct {
name string
want bool
}{{"Embedded", true}, {"Promoted", true}, {"Named", false}} {
field, ok := s.Field(tt.name)
if !ok {
t.Fatalf("field %s missing", tt.name)
}
if got := field.IsEmbedded(); got != tt.want {
t.Errorf("%s: got %v, want %v", tt.name, got, tt.want)
}
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ func (f *Field) Value() any {
// IsEmbedded returns true if the given field is an embedded field.
// Play: https://go.dev/play/p/wV2PrbYm3Ec
func (f *Field) IsEmbedded() bool {
return len(f.field.Index) > 1
return f.field.Anonymous || len(f.field.Index) > 1
}
// IsExported returns true if the given field is exported.
+12 -10
View File
@@ -35,7 +35,7 @@ func CamelCase(s string) string {
}
// UpperCamelCase coverts string to UpperCamelCase string. Non letters and numbers will be ignored.
// Play: https://go.dev/play/p/todo
// Play: https://go.dev/play/p/vPHszjTXSFp
func UpperCamelCase(s string) string {
var builder strings.Builder
@@ -316,10 +316,10 @@ func Substring(s string, offset int, length uint) string {
return ""
}
end := offset + int(length)
if end > size {
end = size
if length > uint(size-offset) {
length = uint(size - offset)
}
end := offset + int(length)
return strings.ReplaceAll(string(rs[offset:end]), "\x00", "")
}
@@ -483,7 +483,10 @@ func IndexOffset(str string, substr string, idxFrom int) int {
return -1
}
return strings.Index(str[idxFrom:], substr) + idxFrom
if index := strings.Index(str[idxFrom:], substr); index >= 0 {
return index + idxFrom
}
return -1
}
// ReplaceWithMap returns a copy of `str`,
@@ -516,7 +519,7 @@ func SplitAndTrim(str, delimiter string, characterMask ...string) []string {
var (
// DefaultTrimChars are the characters which are stripped by Trim* functions in default.
DefaultTrimChars = string([]byte{
DefaultTrimChars = string([]rune{
'\t', // Tab.
'\v', // Vertical tab.
'\n', // New line (line feed).
@@ -629,12 +632,11 @@ func SubInBetween(str string, start string, end string) string {
// This func returns an error if the input strings are of unequal lengths.
// Play: https://go.dev/play/p/glNdQEA9HUi
func HammingDistance(a, b string) (int, error) {
if len(a) != len(b) {
return -1, errors.New("a length and b length are unequal")
}
ar := []rune(a)
br := []rune(b)
if len(ar) != len(br) {
return -1, errors.New("a length and b length are unequal")
}
var distance int
for i, codepoint := range ar {
+13
View File
@@ -482,6 +482,8 @@ func TestSubstring(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestSubstring")
assert.Equal("好世界", Substring("你好世界", 1, ^uint(0)))
assert.Equal("界", Substring("你好世界", -1, ^uint(0)))
assert.Equal("bcd", Substring("abcde", 1, 3))
assert.Equal("bcde", Substring("abcde", 1, 5))
@@ -658,6 +660,8 @@ func TestIndexOffset(t *testing.T) {
{str, "d", len(str) - 1, len(str) - 1},
{str, "d", len(str), -1},
{str, "f", -1, -1},
{str, "missing", 5, -1},
{str, "foo", 5, -1},
}
for _, tt := range tests {
@@ -682,6 +686,12 @@ func TestReplaceWithMap(t *testing.T) {
func TestTrim(t *testing.T) {
t.Parallel()
if got := Trim("\u0085\u00a0value\u00a0\u0085"); got != "value" {
t.Errorf("Trim Unicode whitespace = %q, want value", got)
}
if got := Trim("\ufffdvalue\ufffd"); got != "\ufffdvalue\ufffd" {
t.Errorf("Trim removed replacement characters: %q", got)
}
assert := internal.NewAssert(t, "TestTrim")
@@ -814,6 +824,9 @@ func TestHammingDistance(t *testing.T) {
{"abc", "def", 3},
{"kitten", "sitting", -1},
{"ö", "ü", 1},
{"abc", "你", -1},
{"你", "abc", -1},
{"a", "你", 1},
{"日本語", "日本語", 0},
{"日本語", "語日本", 3},
}
+6 -6
View File
@@ -29,9 +29,9 @@ var (
chineseMobileMatcher *regexp.Regexp = regexp.MustCompile(`^1(?:3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8\d|9\d)\d{8}$`)
chineseIdMatcher *regexp.Regexp = regexp.MustCompile(`([1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx])|([1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}[0-9Xx])`)
chineseMatcher *regexp.Regexp = regexp.MustCompile("[\u4e00-\u9fa5]")
chinesePhoneMatcher *regexp.Regexp = regexp.MustCompile(`\d{3}-\d{8}|\d{4}-\d{7}|\d{4}-\d{8}`)
creditCardMatcher *regexp.Regexp = regexp.MustCompile(`^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11}|6[27][0-9]{14})$`)
base64Matcher *regexp.Regexp = regexp.MustCompile(`^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$`)
chinesePhoneMatcher *regexp.Regexp = regexp.MustCompile(`^(?:\d{3}-\d{8}|\d{4}-\d{7}|\d{4}-\d{8})$`)
creditCardMatcher *regexp.Regexp = regexp.MustCompile(`^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11}|6[27][0-9]{14})$`)
base64Matcher *regexp.Regexp = regexp.MustCompile(`^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$`)
base64URLMatcher *regexp.Regexp = regexp.MustCompile(`^([A-Za-z0-9_-]{4})*([A-Za-z0-9_-]{2}(==)?|[A-Za-z0-9_-]{3}=?)?$`)
binMatcher *regexp.Regexp = regexp.MustCompile(`^(0b)?[01]+$`)
hexMatcher *regexp.Regexp = regexp.MustCompile(`^(#|0x|0X)?[0-9a-fA-F]+$`)
@@ -278,7 +278,7 @@ func IsUrl(str string) bool {
}
u, err := url.Parse(str)
if err != nil || u.Scheme == "" || u.Host == "" {
if err != nil || u.Scheme == "" {
return false
}
@@ -298,7 +298,7 @@ func IsUrl(str string) bool {
}
if u.Scheme == "file" || u.Scheme == "mailto" || u.Scheme == "data" {
return true
return u.Host != "" || u.Path != "" || u.Opaque != ""
}
host := u.Hostname()
@@ -505,7 +505,7 @@ func IsGBK(data []byte) bool {
i+1 < len(data) &&
data[i+1] >= 0x40 &&
data[i+1] <= 0xfe &&
data[i+1] != 0xf7 {
data[i+1] != 0x7f {
i += 2
continue
} else {
+16
View File
@@ -436,6 +436,11 @@ func TestIsUrl(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestIsUrl")
assert.Equal(true, IsUrl("mailto:user@example.com"))
assert.Equal(true, IsUrl("file:///tmp/file.txt"))
assert.Equal(true, IsUrl("data:text/plain,hello"))
assert.Equal(false, IsUrl("mailto:"))
assert.Equal(false, IsUrl("http:/path"))
tests := []struct {
input string
@@ -549,6 +554,9 @@ func TestIsChinesePhone(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestIsChinesePhone")
for _, phone := range []string{"x010-32116675", "0464-8756213x", "0731-822515456", "010-32116675\n"} {
assert.Equal(false, IsChinesePhone(phone))
}
tests := []struct {
input string
@@ -593,6 +601,8 @@ func TestIsCreditCard(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestIsCreditCard")
assert.Equal(true, IsCreditCard("3530111333300000"))
assert.Equal(true, IsCreditCard("213100000000000"))
assert.Equal(true, IsCreditCard("4111111111111111"))
assert.Equal(false, IsCreditCard("123456"))
@@ -602,6 +612,9 @@ func TestIsBase64(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestIsBase64")
assert.Equal(false, IsBase64(`ab\=`))
assert.Equal(false, IsBase64(`\\\\`))
assert.Equal(true, IsBase64("////"))
assert.Equal(true, IsBase64("aGVsbG8="))
assert.Equal(false, IsBase64("123456"))
@@ -798,6 +811,9 @@ func TestIsGBK(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestIsGBK")
if !IsGBK([]byte{0x81, 0xf7}) || IsGBK([]byte{0x81, 0x7f}) {
t.Error("GBK trails must allow 0xf7 and reject 0x7f")
}
str := "你好"
gbkData, _ := simplifiedchinese.GBK.NewEncoder().Bytes([]byte(str))