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

Compare commits

...

7 Commits

Author SHA1 Message Date
dudaodong
db479ef1bc fix: fix issue #292 2025-02-14 17:35:48 +08:00
dudaodong
df8121fbbd Merge branch 'rc' into v2 2025-02-14 17:13:11 +08:00
dudaodong
0e7297cb97 feat: add ShuffleCopy 2025-02-14 16:33:22 +08:00
dudaodong
2e619e48a3 feat: add ReverseCopy 2025-02-14 16:23:46 +08:00
dudaodong
23e61f1acf update pem file 2025-01-14 15:58:58 +08:00
dudaodong
cb308f628c feat: add eventbus 2025-01-14 15:57:43 +08:00
dudaodong
7653afa919 feat: add FindAllOccurrences 2025-01-10 14:03:45 +08:00
16 changed files with 818 additions and 76 deletions

View File

@@ -1,51 +1,51 @@
-----BEGIN rsa private key-----
MIIJKAIBAAKCAgEA3aIM62NSgImSuiZwlvnVY9U+nJbq/77e5TMQa8h0OiY8d/c4
OfyCHM2vQIlxl28a3WpbUkI3kebkDEefLDoBxaL1+A+x8S5Lvy0zfNk/Eo2vxNmJ
OnWF8FLAPFhZq7DzGIGsmgDHe9uXDhsbosfq34chnYOQuZ9/mFy9+Jv8axI+Cgbr
d0N6MVlZ50LEQ3x5SY0xVnCPDxPFKPumvZNeD3prp5BvX4v3IoA5Xx1Kkm2tM4yI
kJrfZFX+B+BGjpyCt6HU+yz+i7gtSjtZ5IvpnpvsJ0SseSz9AXCIfofFxqpO40Vr
GRGK3bKyTDaZjvybt7+5IMb4dUyzKMdjujjLStKP0S+W2phEaNWHLEzXA3AXOJ+n
RN6oplso0BNtyiC9sWKSJUWbUqmdZXmbVuB6vZDb5Ae9KLgeHeb37HUdFbNs5rus
pfdoDaXZzUI2zM2M8A0NRzxQJUGbDxqyPvPUEr84O1P6A6WQ1JlOpljGB6fprgF7
Su/HHYesEYPbTUoV/WrcYTVdoOiC7K5jNgRVapbng8NNDvaQvsJwKgeRDonzxsMG
21Iqd0/ekaRp+t8BwX6kHJtkadGUkF5aDoWecnyk8baN/JAVuGv4IN8K7dtKBOTm
KxPFtyPHAl6rUlc/oKnQ/36MoRTXJwiqRCGnaSrjaU++9M2VC6zBecCERYECAwEA
AQKCAgEAjdRM9jlKK41eQxekR0k7gDaPab++RMkNdJj38jGGB0w+t/qRlbH8RZhu
hRsvgNwN0hFkvUA4tXqPBziyKKg6SBJf202X7qJUwNOZNlUD4sie6ZbYFXvtqXwb
HsLfJ1sGRfF91dOX1LASe2lnhwTuTfr4zQbLj639BjCbNUQFBTPYVaxV9K1OvdPT
D4YPeKxoJWRgZVOEiP561h4sdvaeY8NQrxtj2j4EeaSakj55YTkkdG+DWR5yxI+v
D7U7EboggIjkdZQ2lIzZFr7iaLoMV36qYfq1cJoUkl5ESsxyCQ8lipT600EBn5vi
M5lhLTqEH9NmEg6iItZhdEAclqgPlvI4wXjdqwM080zYwlcWGM5tu7c0C+kPryC/
PnUTxGx+IDh92H/iuedoPuYKB3cQtMCFsmo7MyamSJXGhU/cxsSHNZR6P5mM2X8j
Kg25x95m+hiM8NoqlhAsDVKd/yb1WeoZSjwdQWllLSp98jgKeuG8bK7Ww9ZY/brv
Pp6GRivRce6WHwgKwRv0Srt8VIzdpT+/DZCkgxkUaBRHMN1Gd4QaFqc9d0ZQmgnN
oJoVL0FbWMiRHnbQOF0Y91oBut8vGYEIvOujqEhwvSsLus0oQpHGn7YpOxgtr3Nl
LJiQhwVJ7Ye/7u8A1BQR6BuO6DC0YE1KwdFGeomcupvD9Ke1NJECggEBAPMgsMpq
ykI+kesCeTljGmg4o+ApbH+pGlySUWvLraBXfKr17t7k+4q5t8BzuITK0IFdtRxc
mI5HM5A6UcImmq5NVWVckn0UoYDY6dilccqIX7OBjNBPz85DjzA6QNddOXxVjOqq
LbTZ01dCszbHxJDe2SJ9MGIMbgqYosKREh5M+T8vsAZsBtZgXfLqjjDS9QRWmOAx
KWUAJsf4IgbbZ2+GVOjs2qw9BdRo3tvXxQTjfOBO0Jpoe3COTuLiJDMF1FclbETt
B339JImevoAQCGxVJAcDuI+Qhz+j+NSEpJlinkI0V+l5F6BTnQeQopifllPO0Q01
3fjr6uhKeWMmZ40CggEBAOleBlOu2eBAHk+YiWXqosBAuNFK59/2TEiH3KmDDoP1
71WPQOh0H9ERNp5F0ZCdZrOit3sbVYvxJ86KBJpZvEuhx6EED6uL7xcAsqv5sHfW
z4F3Z7qbdkrHgceYEjM3n4aiOEz0js4hq7LkfpyT4VZrWmivrJAnU2u7+1u+JNE3
AfXyKL5TCNyyfPVhbFJ8aM5znB2UZ9IKFSnMZrxRMuPMVzMOUn5JyLUaVGXCW4LT
bL3FGXZX8X8K2wTb7Pp7zVxkDyZvU0fkLrQwx9vIwK++jIP0AdzaV4r9HM5Iynsb
8Z1DoNHCN7KIF3bMoqeaQUDSTCQ7oRaNhh3xA4dYbsUCggEAQ0fQpLNYtWxLRRWy
Jkdej2jdMLNF6y4ItYVoMsRyj+SmA0l7iQMk+Qbb6s4bSeQ2PxaHgAm/zd+2TTtW
VLwKIiIUd7BeeW60IsvkKqfeDYYftbUsGpl7kEDx5w630uFhfx7NmELv0xRUf9ld
btNpeg2xWPH76aY27Ye/wsgSk4AJmYrA04YhfkG8vfRa1PgMBd9Q/vmb0u9vy/bG
s88TmLE73hltiix46Ib85SmYw/mQHSKyZ4hyYHuBKRgbnGMIl/UrOQe/AwaCjfL4
FMhbDF+jUK2e7Vu5kcr2mRj709aOpROHIHz6JMv+sJE97a58E0UwZM97Vd8zaoTx
gpamIQKCAQArrWdtvioVKKsDpr8AjjvL09FDist/RW/dm2AXceoDlMIot1kkqKdT
z+7zDIo+kNcqA+hnaCRIvuf+ZiKaaPUvCqZ8YnA0YUpsebr3KRJ4O4I27wxBBtvK
/zAxFStC3sRCxJXZAWTA+9hQ8ScpUxw3unv/X/HiQRoB7fsLnrjxV2RMjfhGNvBP
rjBpFMTbY2GSUl0DxETyMOTpH9KSqHfn3tTrP2D9Nf4Ut0rYiNnr0Hpnwj4Twj32
0ydO74KZFxbGlgun2+owaGq9WuvtHNPDkNxnzgGTPmJoJxt/GGydQgukrYWp/LnD
9mi92WsQB3TzFukdVvO9btuNOxC4AjspAoIBAFeJ8ND4aVzHHfh9BJKTytlFMNXY
Z7VrCNxXN5ab1L2DL1JeUWAF0pwQifkcSUWB0nXvf5+zfhclIOkCOQSjQgVEZbF1
uma21nrLd+EgrqaF3XtBIiYfZCiGUcI2dj+WT15ZBiSEqX5VbhTMBD37GyvXIZBp
Gs95zje9JJ8kDtaTLYf08FtveLAyPwt4WaQnNY+5dE6wHQij+ueMgNlnxy1i0CRK
xzrxTxksQkdJ0BQTiRlAsqwTpEI8toCeeUZk3zOcX4maxjlabiGp7ZAgqyEkP8wA
guchSFfon1+Ukf6jnOhGNFVZwL2cXxlDiZGozTdztLc5GF2IQ/NagL7eyyo=
MIIJKQIBAAKCAgEAof7+5KVH6s/ID7onVeuCPVGPIW6TiHHjDlxbYLdA/CyhTvR2
Pk8JvX9BjBXLI/+jTWsyTUucwszu3MaVhIS/gzoLKjr5meVp+HEcFe98s4XRYhob
2cdpDo+Jty8Vuodz5T2nYkIZP/oyJNmQCBYDe20nBwmjjFfworEvwNy2fSucs8wr
lJT2Xqy4oHZqERWvnIJZtBshX9//9Pw3m/GupHeBFBNW2omWCQm4aTEbeepkXloT
gVQ75FEpyyjNjDi+OG/Om2v5l+XCvOcV39Hc7sBovDFNX6w39zQSIKiMpFkxciKP
9SPbqZz+Vv0iyUPKxi2gXwbgy0+k2aTQlsPS976+2/4QgLReU3Ujeh8Q6oRwB8HR
fPlD7aY+dGPMT16o1R3nhs2jwsGId812lw5qgjaYZJCXCBhQcZFs+1IM7c6GmIGX
YIAcFDWiVsENqqesQO5tCA1UTTGtIa9kMYD/pwFK1ANVvrr2c6Qu/0UuBneawok7
6vDHTQCWmVcF0QIe5RaTj8YK9TaNAcQDPy9jM0jWxJE1EBuRpDtIymQIcgMpXwgu
erh9NuXNoGEVPn4mvlVfy+1TVGzws8WsZwY/LH07afdfIeDc1YwLadwq5S+wxuzw
NuSCM9t2rIzfbRhorlg/lVDWqP8H6d9vF5MJY9kSiO3KgCSJLYfVl+Q+8asCAwEA
AQKCAgAdzYTlWcb+WxWqVwwPkZFXaJ7VfrtjudgU90bUZ0JsYmWW2gC6+92F4FiV
xhimWcyYXKVXdRa0+/Dh6yLsy2NUaCRPs6Ph/UPesiiBnJqriG36B2WiTj50sFGc
wuvTckIPJaWavSBaFdSN1Pzbj/k6Bt3MPKi7FB6wP7rSV4i3RIPCzEgkQLeGuW4K
D176H6w8Nfr82JTuR46WaqRsay0/EsFLiTdMY02YAhLMP32Xk1i9xwKZo36VRZ1T
xAD00CemyGMRUu8LU/jcugLbN4fW4M0j+koK1OtC7nB7U6b0QXiIT/V+Gwe5j6l3
JYD12CQBC7naYbCPlup0JA8//WzdXlnh5Tix8XfSVC7hx+0e4caAKJZNHXYEiXY/
7N2Eg77+zk1h+K8oD+8/UCQRSvox1867GkeUd+yDQGpIFr42dM5fSupHt8wV7Etm
Oap8yI2eX+NUr+gZcMlpaNt3bVFfAQNPrMmMqAsJvlGLTMiniLJ5gE5GhRp+A+03
mCmokS92qa9E2m9GyaSYWlu37VnMgFeoLxgEj3tiVmiHiXbtReM2F2SLybS/kHxx
XrgeqlU85J6C7P9ggxEoph2OxGz5+DuinQL2V/JPrV2JhTGEcTHqbV2lN3yfCKj7
uyzmS7wPTmyM5iJCnDj+DRVC6Od3BRYeh7ohte40zBsNZFLZYQKCAQEAwzrjWUoQ
ORa/kSA+n1d4CpDPcYX93JhKLPCmSDvE1MJqNE0uqfSbHajtt9XNrRMjmZ7Godnm
pzMS9hpVc3+DOt4uNOn0I0AY6fbZY4Wn/PJkqRe7RcCeLvvkKu6m4qGmapbu84xl
FwjQhMksglrx96uB8YIvvtsypyxJvdxYuB7TXnQHU8uXqFgmBkwu+/VmUE/xqIe/
yEh6LQ9Yiqee9U/WGQwLmupVr0kvixlNP1tlT52ISdD4+3vuUlOlVIcdXefknih4
tOiUkOWx+u4sIyQ++d0cf1CyMAX8vStikepNoiafGbpRT0ON/EpBC8Aimn15WOUk
rhBFkQA874628QKCAQEA1GvTH6f9Du9svOXeYJmEgTBkpSwvWLv81McpZ6cErvt9
YOIKYndGvmhABHFi9Cy9E5FITUBU2zFNmd4idgS9FfcZx6TPibpKp46lHHW4Pwa+
qXjJ1Xerre938Yh0HeTM7AIUUrTfZHMb7Ymr+8rQW3T90jnJuzCji28eoMzhemNw
wK5qiTfjC7mKS1WOxCfCAwiLCtCGSaW94WfL3/4vFGEKgMwJBj/FI34uJCbYVHmI
wZKg0Tdf3xCv+oz9spLfYyaz0AobiCGzBw3gaxlQl2FqeQzOUQal+KQf78scXlBz
eHZxB9gFl3FrR3thuZ7SyAnTj4gCj6JxT3tySEaKWwKCAQEAuhi9NJTT6AdWLjmF
WBBhvfiRtU4bUhbcxf/TEZHfq0tzP02/SISBBAHOL+me9/cBfWMLRqbWJdUaovsx
LzqCVjAJ7aiBbsSfuw3x5Ns36XcJGuIjQnc8kd7MfVwmOmwKnRooxoGyrwVY9upp
Ag09D3AuGo+VgaGipBYkaNXMwB2qMCP8BBVTCEaWYHRoaQZgM/gwjAxydLEZvTAe
n3TuojorBI8l1NLBQKhLqJVCvD0b0ouAqZSIcfiNkW5ob62oAaaVl2lOvmvhiklZ
oa885XacjUMG8hly8TIT3CKqABtPS1zzVevzq9HiW3ZQkKnikk8+x80NbNrX5UNL
0rLAkQKCAQEAyqpBGN/OqbQ225a47xMo/5TrQUeBuLhKhbuqvlD0P/qDaa7f14gT
P9D42wRPM8WHc6bWA5ZQH3zPm/D6kfz9ZnqF5xtQQwgw9+I5l4idC8zklY4/iuIN
MvrZReE5X9gOx1FIkIwu4oiMabpDEw0ycz+Qd0VZObYzIvIWl7ZBneJIDh2aWWav
wVz5G9z6RB3mlbxN5DiFFrkCC49bTU8XHetj+PQx2/t1m+JkJWvGU/pMRtsdgd7C
InGqZHKmDfzhEpk3T1KFaAE27JEJv0S8xmFUEz/rjBS6vxnfuonffABn2tOuDZzF
4PJ+Nwn5d/6W/fGaHkPWpbGHSBzwMRLUJwKCAQBiPAfbLKuB9u2HFxV33lQuBlMi
HK6JhLoN0o0I5ICuC01IlOaAjAbdTAZdNXcuj9JPfS4tuLG3+nRueKJ5A7MHvwDG
ZBvVq1q8kKnEl04yW4J/2SfjFk+Do2V3WR8vYjphOgd2pXP72IRw5YWW087VCKFY
x2Cjuluapss8zdPR6lBxrrdvSCoysW154ALchgUeJAv4G9x/KPQBoBUkDXO4R6UJ
a36nUMBO7XjkFeucU9npdOl9UEz3ZOE9mWLtck60lZofHoS/dPaY7hzAjetbtfDs
J8sSwmHsb1DdOx5YSpEkg9aPrtShkChYoKOJFLKhpf7WGbVx2U7OBw71BCa5
-----END rsa private key-----

View File

@@ -1,14 +1,14 @@
-----BEGIN rsa public key-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3aIM62NSgImSuiZwlvnV
Y9U+nJbq/77e5TMQa8h0OiY8d/c4OfyCHM2vQIlxl28a3WpbUkI3kebkDEefLDoB
xaL1+A+x8S5Lvy0zfNk/Eo2vxNmJOnWF8FLAPFhZq7DzGIGsmgDHe9uXDhsbosfq
34chnYOQuZ9/mFy9+Jv8axI+Cgbrd0N6MVlZ50LEQ3x5SY0xVnCPDxPFKPumvZNe
D3prp5BvX4v3IoA5Xx1Kkm2tM4yIkJrfZFX+B+BGjpyCt6HU+yz+i7gtSjtZ5Ivp
npvsJ0SseSz9AXCIfofFxqpO40VrGRGK3bKyTDaZjvybt7+5IMb4dUyzKMdjujjL
StKP0S+W2phEaNWHLEzXA3AXOJ+nRN6oplso0BNtyiC9sWKSJUWbUqmdZXmbVuB6
vZDb5Ae9KLgeHeb37HUdFbNs5ruspfdoDaXZzUI2zM2M8A0NRzxQJUGbDxqyPvPU
Er84O1P6A6WQ1JlOpljGB6fprgF7Su/HHYesEYPbTUoV/WrcYTVdoOiC7K5jNgRV
apbng8NNDvaQvsJwKgeRDonzxsMG21Iqd0/ekaRp+t8BwX6kHJtkadGUkF5aDoWe
cnyk8baN/JAVuGv4IN8K7dtKBOTmKxPFtyPHAl6rUlc/oKnQ/36MoRTXJwiqRCGn
aSrjaU++9M2VC6zBecCERYECAwEAAQ==
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAof7+5KVH6s/ID7onVeuC
PVGPIW6TiHHjDlxbYLdA/CyhTvR2Pk8JvX9BjBXLI/+jTWsyTUucwszu3MaVhIS/
gzoLKjr5meVp+HEcFe98s4XRYhob2cdpDo+Jty8Vuodz5T2nYkIZP/oyJNmQCBYD
e20nBwmjjFfworEvwNy2fSucs8wrlJT2Xqy4oHZqERWvnIJZtBshX9//9Pw3m/Gu
pHeBFBNW2omWCQm4aTEbeepkXloTgVQ75FEpyyjNjDi+OG/Om2v5l+XCvOcV39Hc
7sBovDFNX6w39zQSIKiMpFkxciKP9SPbqZz+Vv0iyUPKxi2gXwbgy0+k2aTQlsPS
976+2/4QgLReU3Ujeh8Q6oRwB8HRfPlD7aY+dGPMT16o1R3nhs2jwsGId812lw5q
gjaYZJCXCBhQcZFs+1IM7c6GmIGXYIAcFDWiVsENqqesQO5tCA1UTTGtIa9kMYD/
pwFK1ANVvrr2c6Qu/0UuBneawok76vDHTQCWmVcF0QIe5RaTj8YK9TaNAcQDPy9j
M0jWxJE1EBuRpDtIymQIcgMpXwguerh9NuXNoGEVPn4mvlVfy+1TVGzws8WsZwY/
LH07afdfIeDc1YwLadwq5S+wxuzwNuSCM9t2rIzfbRhorlg/lVDWqP8H6d9vF5MJ
Y9kSiO3KgCSJLYfVl+Q+8asCAwEAAQ==
-----END rsa public key-----

View File

@@ -70,6 +70,7 @@ import (
- [FlatMap](#FlatMap)
- [Merge](#Merge)
- [Reverse](#Reverse)
- [ReverseCopy](#ReverseCopy)
- [Reduce<sup>deprecated</sup>](#Reduce)
- [ReduceConcurrent](#ReduceConcurrent)
- [ReduceBy](#ReduceBy)
@@ -78,6 +79,7 @@ import (
- [ReplaceAll](#ReplaceAll)
- [Repeat](#Repeat)
- [Shuffle](#Shuffle)
- [ShuffleCopy](#ShuffleCopy)
- [IsAscending](#IsAscending)
- [IsDescending](#IsDescending)
- [IsSorted](#IsSorted)
@@ -1760,6 +1762,38 @@ func main() {
}
```
### <span id="ReverseCopy">ReverseCopy</span>
<p>反转切片中的元素顺序, 不改变原slice。</p>
<b>函数签名:</b>
```go
func ReverseCopy[T any](slice []T) []T
```
<b>示例:<span style="float:right;display:inline-block;">[运行](todo)</span></b>
```go
import (
"fmt"
"github.com/duke-git/lancet/v2/slice"
)
func main() {
strs := []string{"a", "b", "c", "d"}
reversedStrs := slice.ReverseCopy(strs)
fmt.Println(reversedStrs)
fmt.Println(strs)
// Output:
// [d c b a]
// [a b c d]
}
```
### <span id="Reduce">Reduce</span>
<p>将切片中的元素依次运行iteratee函数返回运行结果。</p>
@@ -1994,7 +2028,7 @@ func main() {
### <span id="Shuffle">Shuffle</span>
<p>随机打乱切片中的元素顺序</p>
<p>随机打乱切片中的元素顺序</p>
<b>函数签名:</b>
@@ -2021,6 +2055,37 @@ func main() {
}
```
### <span id="ShuffleCopy">ShuffleCopy</span>
<p>随机打乱切片中的元素顺序, 不改变原切片。</p>
<b>函数签名:</b>
```go
func ShuffleCopy[T any](slice []T) []T
```
<b>示例:<span style="float:right;display:inline-block;">[运行](todo)</span></b>
```go
import (
"fmt"
"github.com/duke-git/lancet/v2/slice"
)
func main() {
nums := []int{1, 2, 3, 4, 5}
result := slice.ShuffleCopy(nums)
fmt.Println(result)
fmt.Println(nums)
// Output:
// [3 1 5 4 2] (random order)
// [1 2 3 4 5]
}
```
### <span id="IsAscending">IsAscending</span>
<p>检查切片元素是否按升序排列。</p>

View File

@@ -69,6 +69,7 @@ import (
- [TemplateReplace](#TemplateReplace)
- [RegexMatchAllGroups](#RegexMatchAllGroups)
- [ExtractContent](#ExtractContent)
- [FindAllOccurrences](#FindAllOccurrences)
<div STYLE="page-break-after: always;"></div>
@@ -1708,7 +1709,7 @@ func main() {
func RegexMatchAllGroups(pattern, str string) [][]string
```
<b>示例:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/JZiu0RXpgN-)</span></b>
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/JZiu0RXpgN-)</span></b>
```go
import (
@@ -1741,7 +1742,7 @@ func main() {
func ExtractContent(s, start, end string) []string
```
<b>示例:<span style="float:right;display:inline-block;">[Run](https://go.dev/play/p/Ay9UIk7Rum9)</span></b>
<b>示例:<span style="float:right;display:inline-block;">[运行](https://go.dev/play/p/Ay9UIk7Rum9)</span></b>
```go
import (
@@ -1759,4 +1760,32 @@ func main() {
// Output:
// [content1 content2 content1]
}
```
### <span id="FindAllOccurrences">FindAllOccurrences</span>
<p>返回子字符串在字符串中所有出现的位置。</p>
<b>函数签名:</b>
```go
func FindAllOccurrences(str, substr string) []int
```
<b>示例:<span style="float:right;display:inline-block;">[运行](todo)</span></b>
```go
import (
"fmt"
"github.com/duke-git/lancet/v2/strutil"
)
func main() {
result := strutil.FindAllOccurrences("ababab", "ab")
fmt.Println(result)
// Output:
// [0 2 4]
}
```

View File

@@ -70,6 +70,7 @@ import (
- [FlatMap](#FlatMap)
- [Merge](#Merge)
- [Reverse](#Reverse)
- [ReverseCopy](#ReverseCopy)
- [Reduce<sup>deprecated</sup>](#Reduce)
- [ReduceConcurrent](#ReduceConcurrent)
- [ReduceBy](#ReduceBy)
@@ -78,6 +79,7 @@ import (
- [ReplaceAll](#ReplaceAll)
- [Repeat](#Repeat)
- [Shuffle](#Shuffle)
- [ShuffleCopy](#ShuffleCopy)
- [IsAscending](#IsAscending)
- [IsDescending](#IsDescending)
- [IsSorted](#IsSorted)
@@ -1756,6 +1758,38 @@ func main() {
}
```
### <span id="ReverseCopy">ReverseCopy</span>
<p>Return a new slice of element order is reversed to the given slice.</p>
<b>Signature:</b>
```go
func ReverseCopy[T any](slice []T) []T
```
<b>Example:<span style="float:right;display:inline-block;">[Run](todo)</span></b>
```go
import (
"fmt"
"github.com/duke-git/lancet/v2/slice"
)
func main() {
strs := []string{"a", "b", "c", "d"}
reversedStrs := slice.ReverseCopy(strs)
fmt.Println(reversedStrs)
fmt.Println(strs)
// Output:
// [d c b a]
// [a b c d]
}
```
### <span id="Reduce">Reduce</span>
<p>Reduce slice.</p>
@@ -2018,6 +2052,37 @@ func main() {
}
```
### <span id="ShuffleCopy">ShuffleCopy</span>
<p>Return a new slice with elements shuffled.</p>
<b>Signature:</b>
```go
func ShuffleCopy[T any](slice []T) []T
```
<b>Example:<span style="float:right;display:inline-block;">[Run](todo)</span></b>
```go
import (
"fmt"
"github.com/duke-git/lancet/v2/slice"
)
func main() {
nums := []int{1, 2, 3, 4, 5}
result := slice.ShuffleCopy(nums)
fmt.Println(result)
fmt.Println(nums)
// Output:
// [3 1 5 4 2] (random order)
// [1 2 3 4 5]
}
```
### <span id="IsAscending">IsAscending</span>
<p>Checks if a slice is ascending order.</p>

View File

@@ -68,7 +68,8 @@ import (
- [Rotate](#Rotate)
- [TemplateReplace](#TemplateReplace)
- [RegexMatchAllGroups](#RegexMatchAllGroups)
- [ExtractContent](#RegexMatchAllGroups)
- [ExtractContent](#ExtractContent)
- [FindAllOccurrences](#FindAllOccurrences)
<div STYLE="page-break-after: always;"></div>
@@ -1761,4 +1762,32 @@ func main() {
// Output:
// [content1 content2 content1]
}
```
### <span id="FindAllOccurrences">FindAllOccurrences</span>
<p>Returns the positions of all occurrences of a substring in a string.</p>
<b>Signature:</b>
```go
func FindAllOccurrences(str, substr string) []int
```
<b>Example:<span style="float:right;display:inline-block;">[Run](todo)</span></b>
```go
import (
"fmt"
"github.com/duke-git/lancet/v2/strutil"
)
func main() {
result := strutil.FindAllOccurrences("ababab", "ab")
fmt.Println(result)
// Output:
// [0 2 4]
}
```

185
eventbus/eventbus.go Normal file
View File

@@ -0,0 +1,185 @@
// Copyright 2025 dudaodong@gmail.com. All rights reserved.
// Use of this source code is governed by MIT license
// Package eventbus implements a simple event bus.
package eventbus
import (
"fmt"
"sort"
"sync"
)
// Event is the struct that is passed to the event listener, now it directly uses the generic Payload type.
type Event[T any] struct {
Topic string
Payload T
}
// EventBus is the struct that holds the listeners and the error handler.
type EventBus[T any] struct {
// listeners map[string][]*EventListener[T]
listeners sync.Map
mu sync.RWMutex
errorHandler func(err error)
}
// EventListener is the struct that holds the listener function and its priority.
type EventListener[T any] struct {
priority int
listener func(eventData T)
async bool
filter func(eventData T) bool
}
// NewEventBus creates a new EventBus.
func NewEventBus[T any]() *EventBus[T] {
return &EventBus[T]{
listeners: sync.Map{},
}
}
// Subscribe subscribes to an event with a specific event topic and listener function.
func (eb *EventBus[T]) Subscribe(topic string, listener func(eventData T), async bool, priority int, filter func(eventData T) bool) {
eb.mu.Lock()
defer eb.mu.Unlock()
el := &EventListener[T]{
priority: priority,
listener: listener,
async: async,
filter: filter,
}
listenersInterface, _ := eb.listeners.LoadOrStore(topic, []*EventListener[T]{})
listeners := listenersInterface.([]*EventListener[T])
listeners = append(listeners, el)
sort.Slice(listeners, func(i, j int) bool {
return listeners[i].priority > listeners[j].priority
})
eb.listeners.Store(topic, listeners)
}
// Unsubscribe unsubscribes from an event with a specific event topic and listener function.
func (eb *EventBus[T]) Unsubscribe(topic string, listener func(eventData T)) {
eb.mu.Lock()
defer eb.mu.Unlock()
listenersInterface, ok := eb.listeners.Load(topic)
if !ok {
return
}
listeners := listenersInterface.([]*EventListener[T])
listenerPtr := fmt.Sprintf("%p", listener)
var updatedListeners []*EventListener[T]
for _, l := range listeners {
if fmt.Sprintf("%p", l.listener) != listenerPtr {
updatedListeners = append(updatedListeners, l)
}
}
eb.listeners.Store(topic, updatedListeners)
}
// Publish publishes an event with a specific event topic and data payload.
func (eb *EventBus[T]) Publish(event Event[T]) {
eb.mu.RLock()
defer eb.mu.RUnlock()
listenersInterface, exists := eb.listeners.Load(event.Topic)
if !exists {
return
}
listeners := listenersInterface.([]*EventListener[T])
for _, listener := range listeners {
if listener.filter != nil && !listener.filter(event.Payload) {
continue
}
if listener.async {
go eb.publishToListener(listener, event)
} else {
eb.publishToListener(listener, event)
}
}
}
func (eb *EventBus[T]) publishToListener(listener *EventListener[T], event Event[T]) {
defer func() {
if r := recover(); r != nil && eb.errorHandler != nil {
eb.errorHandler(fmt.Errorf("%v", r))
}
}()
listener.listener(event.Payload)
}
// SetErrorHandler sets the error handler function.
func (eb *EventBus[T]) SetErrorHandler(handler func(err error)) {
eb.errorHandler = handler
}
// ClearListeners clears all the listeners.
func (eb *EventBus[T]) ClearListeners() {
eb.mu.Lock()
defer eb.mu.Unlock()
eb.listeners = sync.Map{}
}
// ClearListenersByTopic clears all the listeners by topic.
func (eb *EventBus[T]) ClearListenersByTopic(topic string) {
eb.mu.Lock()
defer eb.mu.Unlock()
eb.listeners.Delete(topic)
}
// GetListenersCount returns the number of listeners for a specific event topic.
func (eb *EventBus[T]) GetListenersCount(topic string) int {
eb.mu.RLock()
defer eb.mu.RUnlock()
listenersInterface, ok := eb.listeners.Load(topic)
if !ok {
return 0
}
listeners := listenersInterface.([]*EventListener[T])
return len(listeners)
}
// GetAllListenersCount returns the total number of listeners.
func (eb *EventBus[T]) GetAllListenersCount() int {
eb.mu.RLock()
defer eb.mu.RUnlock()
count := 0
eb.listeners.Range(func(key, value interface{}) bool {
count += len(value.([]*EventListener[T]))
return true
})
return count
}
// GetEvents returns all the events topics.
func (eb *EventBus[T]) GetEvents() []string {
eb.mu.RLock()
defer eb.mu.RUnlock()
var events []string
eb.listeners.Range(func(key, value interface{}) bool {
events = append(events, key.(string))
return true
})
return events
}

220
eventbus/eventbus_test.go Normal file
View File

@@ -0,0 +1,220 @@
package eventbus
import (
"sync"
"testing"
"time"
"github.com/duke-git/lancet/v2/internal"
)
func TestEventBus_Subscribe(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_Subscribe")
eb := NewEventBus[int]()
eb.Subscribe("event1", func(eventData int) {
assert.Equal(1, eventData)
}, false, 0, nil)
eb.Publish(Event[int]{Topic: "event1", Payload: 1})
}
func TestEventBus_Unsubscribe(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_Unsubscribe")
eb := NewEventBus[int]()
receivedData := 0
listener := func(eventData int) {
receivedData = eventData
}
eb.Subscribe("event1", listener, false, 0, nil)
eb.Unsubscribe("event1", listener)
eb.Publish(Event[int]{Topic: "event1", Payload: 1})
assert.Equal(0, receivedData)
}
func TestEventBus_Subscribe_WithFilter(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_Subscribe_WithFilter")
eb := NewEventBus[int]()
receivedData := 0
listener := func(eventData int) {
receivedData = eventData
}
filter := func(eventData int) bool {
return eventData == 1
}
eb.Subscribe("event1", listener, false, 0, filter)
eb.Publish(Event[int]{Topic: "event1", Payload: 1})
eb.Publish(Event[int]{Topic: "event1", Payload: 2})
assert.Equal(1, receivedData)
}
func TestEventBus_Subscribe_WithPriority(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_Subscribe_WithPriority")
eb := NewEventBus[int]()
var receivedData []int
listener1 := func(eventData int) {
receivedData = append(receivedData, 1)
}
listener2 := func(eventData int) {
receivedData = append(receivedData, 2)
}
eb.Subscribe("event1", listener1, false, 1, nil)
eb.Subscribe("event1", listener2, false, 2, nil)
eb.Publish(Event[int]{Topic: "event1", Payload: 1})
assert.Equal([]int{2, 1}, receivedData)
}
func TestEventBus_Subscribe_Async(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_Subscribe_Async")
eb := NewEventBus[string]()
var wg sync.WaitGroup
wg.Add(1)
eb.Subscribe("event1", func(eventData string) {
time.Sleep(100 * time.Millisecond)
assert.Equal("hello", eventData)
wg.Done()
}, true, 1, nil)
eb.Publish(Event[string]{Topic: "event1", Payload: "hello"})
wg.Wait()
}
func TestEventBus_ErrorHandler(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_ErrorHandler")
eb := NewEventBus[string]()
eb.SetErrorHandler(func(err error) {
assert.Equal("error", err.Error())
})
eb.Subscribe("event1", func(eventData string) {
panic("error")
}, false, 0, nil)
eb.Publish(Event[string]{Topic: "event1", Payload: "hello"})
}
func TestEventBus_ClearListeners(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_ClearListeners")
eb := NewEventBus[int]()
receivedData1 := 0
receivedData2 := 0
eb.Subscribe("event1", func(eventData int) {
receivedData1 = eventData
}, false, 0, nil)
eb.Subscribe("event2", func(eventData int) {
receivedData2 = eventData
}, false, 0, nil)
eb.ClearListeners()
eb.Publish(Event[int]{Topic: "event1", Payload: 1})
eb.Publish(Event[int]{Topic: "event1", Payload: 2})
assert.Equal(0, receivedData1)
assert.Equal(0, receivedData2)
}
func TestEventBus_ClearListenersByTopic(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_ClearListenersByTopic")
eb := NewEventBus[int]()
receivedData1 := 0
receivedData2 := 0
eb.Subscribe("event1", func(eventData int) {
receivedData1 = eventData
}, false, 0, nil)
eb.Subscribe("event2", func(eventData int) {
receivedData2 = eventData
}, false, 0, nil)
eb.ClearListenersByTopic("event1")
eb.Publish(Event[int]{Topic: "event1", Payload: 1})
eb.Publish(Event[int]{Topic: "event2", Payload: 2})
assert.Equal(0, receivedData1)
assert.Equal(2, receivedData2)
}
func TestEventBus_GetListenersCount(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_GetListenersCount")
eb := NewEventBus[int]()
eb.Subscribe("event1", func(eventData int) {}, false, 0, nil)
eb.Subscribe("event1", func(eventData int) {}, false, 0, nil)
eb.Subscribe("event2", func(eventData int) {}, false, 0, nil)
assert.Equal(2, eb.GetListenersCount("event1"))
assert.Equal(1, eb.GetListenersCount("event2"))
}
func TestEventBus_GetAllListenersCount(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_GetAllListenersCount")
eb := NewEventBus[int]()
eb.Subscribe("event1", func(eventData int) {}, false, 0, nil)
eb.Subscribe("event1", func(eventData int) {}, false, 0, nil)
eb.Subscribe("event2", func(eventData int) {}, false, 0, nil)
assert.Equal(3, eb.GetAllListenersCount())
}
func TestEventBus_GetEvents(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestEventBus_GetEvents")
eb := NewEventBus[int]()
eb.Subscribe("event1", func(eventData int) {}, false, 0, nil)
eb.Subscribe("event2", func(eventData int) {}, false, 0, nil)
events := eb.GetEvents()
assert.Equal(2, len(events))
assert.Equal("event1", events[0])
assert.Equal("event2", events[1])
}

View File

@@ -145,10 +145,14 @@ func CeilToString[T constraints.Float | constraints.Integer](x T, n int) string
// Max return max value of numbers.
// Play: https://go.dev/play/p/cN8DHI0rTkH
func Max[T constraints.Integer | constraints.Float](numbers ...T) T {
max := numbers[0]
func Max[T constraints.Ordered](items ...T) T {
if len(items) < 1 {
panic("mathutil.Max: empty list")
}
for _, v := range numbers {
max := items[0]
for _, v := range items {
if max < v {
max = v
}
@@ -181,10 +185,14 @@ func MaxBy[T any](slice []T, comparator func(T, T) bool) T {
// Min return min value of numbers.
// Play: https://go.dev/play/p/21BER_mlGUj
func Min[T constraints.Integer | constraints.Float](numbers ...T) T {
min := numbers[0]
func Min[T constraints.Ordered](items ...T) T {
if len(items) < 1 {
panic("mathutil.min: empty list")
}
for _, v := range numbers {
min := items[0]
for _, v := range items {
if min > v {
min = v
}

View File

@@ -178,6 +178,7 @@ func TestMax(t *testing.T) {
assert.Equal(0, Max(0, 0))
assert.Equal(3, Max(1, 2, 3))
assert.Equal(1.4, Max(1.2, 1.4, 1.1, 1.4))
assert.Equal("abc", Max("a", "ab", "abc"))
type Integer int
assert.Equal(Integer(1), Max(Integer(1), Integer(0)))
@@ -212,6 +213,8 @@ func TestMin(t *testing.T) {
assert.Equal(0, Min(0, 0))
assert.Equal(1, Min(1, 2, 3))
assert.Equal(1.1, Min(1.2, 1.4, 1.1, 1.4))
assert.Equal("a", Min("a", "ab", "abc"))
}
func TestMinBy(t *testing.T) {

View File

@@ -1002,7 +1002,19 @@ func Reverse[T any](slice []T) {
}
}
// Shuffle the slice.
// ReverseCopy return a new slice of element order is reversed to the given slice.
// Play: todo
func ReverseCopy[T any](slice []T) []T {
result := make([]T, len(slice))
for i, j := 0, len(slice)-1; i < len(slice); i, j = i+1, j-1 {
result[i] = slice[j]
}
return result
}
// Shuffle return a new slice with elements shuffled.
// Play: https://go.dev/play/p/YHvhnWGU3Ge
func Shuffle[T any](slice []T) []T {
rand.Seed(time.Now().UnixNano())
@@ -1014,6 +1026,20 @@ func Shuffle[T any](slice []T) []T {
return slice
}
// ShuffleCopy return a new slice with elements shuffled.
// Play: todo
func ShuffleCopy[T any](slice []T) []T {
result := make([]T, len(slice))
copy(result, slice)
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(result), func(i, j int) {
result[i], result[j] = result[j], result[i]
})
return result
}
// IsAscending checks if a slice is ascending order.
// Play: https://go.dev/play/p/9CtsFjet4SH
func IsAscending[T constraints.Ordered](slice []T) bool {

View File

@@ -937,6 +937,41 @@ func ExampleReverse() {
// [d c b a]
}
func ExampleReverseCopy() {
strs := []string{"a", "b", "c", "d"}
reversedStrs := ReverseCopy(strs)
fmt.Println(reversedStrs)
fmt.Println(strs)
// Output:
// [d c b a]
// [a b c d]
}
func ExampleShuffle() {
strs := []string{"a", "b", "c", "d"}
Shuffle(strs)
fmt.Println(len(strs))
// Output:
// 4
}
func ExampleShuffleCopy() {
strs := []string{"a", "b", "c", "d"}
shuffledStrs := ShuffleCopy(strs)
fmt.Println(len(shuffledStrs))
fmt.Println(strs)
// Output:
// 4
// [a b c d]
}
func ExampleIsAscending() {
result1 := IsAscending([]int{1, 2, 3, 4, 5})

View File

@@ -1115,6 +1115,17 @@ func TestReverse(t *testing.T) {
assert.Equal([]string{"e", "d", "c", "b", "a"}, s2)
}
func TestReverseCopy(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestReverseCopy")
numbers := []int{1, 2, 3, 4, 5}
reversedNumbers := ReverseCopy(numbers)
assert.Equal([]int{5, 4, 3, 2, 1}, reversedNumbers)
assert.Equal([]int{1, 2, 3, 4, 5}, numbers)
}
func TestDifference(t *testing.T) {
t.Parallel()
@@ -1330,6 +1341,18 @@ func TestShuffle(t *testing.T) {
assert.Equal(5, len(result))
}
func TestShuffleCopy(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestShuffleCopy")
numbers := []int{1, 2, 3, 4, 5}
result := ShuffleCopy(numbers)
assert.Equal(5, len(result))
assert.Equal([]int{1, 2, 3, 4, 5}, numbers)
}
func TestIndexOf(t *testing.T) {
t.Parallel()

View File

@@ -738,14 +738,14 @@ func RegexMatchAllGroups(pattern, str string) [][]string {
// ExtractContent extracts the content between the start and end strings in the source string.
// Play: https://go.dev/play/p/Ay9UIk7Rum9
func ExtractContent(s, start, end string) []string {
func ExtractContent(str, start, end string) []string {
result := []string{}
for {
if _, after, ok := strings.Cut(s, start); ok {
if _, after, ok := strings.Cut(str, start); ok {
if before, _, ok := strings.Cut(after, end); ok {
result = append(result, before)
s = after
str = after
} else {
break
}
@@ -756,3 +756,20 @@ func ExtractContent(s, start, end string) []string {
return result
}
// FindAllOccurrences returns the positions of all occurrences of a substring in a string.
// Play: todo
func FindAllOccurrences(str, substr string) []int {
var positions []int
for i := 0; i < len(str); {
index := strings.Index(str[i:], substr)
if index == -1 {
break
}
positions = append(positions, i+index)
i += index + 1
}
return positions
}

View File

@@ -763,5 +763,13 @@ func ExampleExtractContent() {
// Output:
// [content1 content2 content1]
}
func ExampleFindAllOccurrences() {
result := FindAllOccurrences("ababab", "ab")
fmt.Println(result)
// Output:
// [0 2 4]
}

View File

@@ -937,3 +937,32 @@ func TestExtractContent(t *testing.T) {
})
}
}
func TestFindAllOccurrences(t *testing.T) {
t.Parallel()
assert := internal.NewAssert(t, "TestFindAllOccurrences")
var empty []int
tests := []struct {
input string
substr string
expected []int
}{
{"", "", empty},
{"", "a", empty},
{"a", "", []int{0}},
{"a", "a", []int{0}},
{"aa", "a", []int{0, 1}},
{"ababab", "ab", []int{0, 2, 4}},
{"ababab", "ba", []int{1, 3}},
{"ababab", "c", empty},
{"ababab", "ababab", []int{0}},
{"ababab", "abababc", empty},
{"ababab", "abababa", empty},
}
for _, tt := range tests {
assert.Equal(tt.expected, FindAllOccurrences(tt.input, tt.substr))
}
}