Compare commits
7 Commits
mapper/v0.
...
query_pars
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bed43b3576 | ||
|
|
b9665d4f9b | ||
|
|
1d53d3af4c | ||
|
|
a502539417 | ||
|
|
0cd2eb95f2 | ||
|
|
2e38fe77d7 | ||
|
|
55c80024c2 |
20
entgo/go.mod
20
entgo/go.mod
@@ -1,26 +1,26 @@
|
||||
module github.com/tx7do/go-utils/entgo
|
||||
|
||||
go 1.23.0
|
||||
go 1.24
|
||||
|
||||
toolchain go1.23.2
|
||||
toolchain go1.24.4
|
||||
|
||||
replace github.com/tx7do/go-utils/id => ../id
|
||||
|
||||
require (
|
||||
entgo.io/contrib v0.6.0
|
||||
entgo.io/ent v0.14.4
|
||||
github.com/XSAM/otelsql v0.38.0
|
||||
github.com/XSAM/otelsql v0.39.0
|
||||
github.com/go-kratos/kratos/v2 v2.8.4
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tx7do/go-utils v1.1.28
|
||||
github.com/tx7do/go-utils v1.1.29
|
||||
github.com/tx7do/go-utils/id v0.0.2
|
||||
go.opentelemetry.io/otel v1.36.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.34.0 // indirect
|
||||
ariga.io/atlas v0.35.0 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||
@@ -43,15 +43,15 @@ require (
|
||||
github.com/sony/sonyflake v1.2.1 // indirect
|
||||
github.com/zclconf/go-cty v1.16.3 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
||||
40
entgo/go.sum
40
entgo/go.sum
@@ -1,13 +1,13 @@
|
||||
ariga.io/atlas v0.34.0 h1:4hdy+2x+xNs6Lx2anuJ/4Q7lCaqddbEj5CtRDVOBu0M=
|
||||
ariga.io/atlas v0.34.0/go.mod h1:WJesu2UCpGQvgUh3oVP94EiRT61nNy1W/VN5g+vqP1I=
|
||||
ariga.io/atlas v0.35.0 h1:tzco6CEZm1/jGD2ifHhKFlsQB7Bfsc/mty4zwm6Mlbc=
|
||||
ariga.io/atlas v0.35.0/go.mod h1:9ZAIr/V85596AVxmN8edyVHYKKpnNsDMdnHLsEliW7k=
|
||||
entgo.io/contrib v0.6.0 h1:xfo4TbJE7sJZWx7BV7YrpSz7IPFvS8MzL3fnfzZjKvQ=
|
||||
entgo.io/contrib v0.6.0/go.mod h1:3qWIseJ/9Wx2Hu5zVh15FDzv7d/UvKNcYKdViywWCQg=
|
||||
entgo.io/ent v0.14.4 h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI=
|
||||
entgo.io/ent v0.14.4/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/XSAM/otelsql v0.38.0 h1:zWU0/YM9cJhPE71zJcQ2EBHwQDp+G4AX2tPpljslaB8=
|
||||
github.com/XSAM/otelsql v0.38.0/go.mod h1:5ePOgcLEkWvZtN9H3GV4BUlPeM3p3pzLDCnRG73X8h8=
|
||||
github.com/XSAM/otelsql v0.39.0 h1:4o374mEIMweaeevL7fd8Q3C710Xi2Jh/c8G4Qy9bvCY=
|
||||
github.com/XSAM/otelsql v0.39.0/go.mod h1:uMOXLUX+wkuAuP0AR3B45NXX7E9lJS2mERa8gqdU8R0=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
@@ -71,34 +71,34 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6
|
||||
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
||||
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
|
||||
|
||||
4
go.mod
4
go.mod
@@ -8,8 +8,8 @@ require (
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
|
||||
8
go.sum
8
go.sum
@@ -22,10 +22,10 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
|
||||
70
query_parser/README.md
Normal file
70
query_parser/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 查询解析器
|
||||
|
||||
## 排序规则
|
||||
|
||||
排序操作本质上是`SQL`里面的`Order By`条件。
|
||||
|
||||
| 序列 | 示例 | 备注 |
|
||||
|----|--------------------|--------------|
|
||||
| 升序 | `["type"]` | |
|
||||
| 降序 | `["-create_time"]` | 字段名前加`-`是为降序 |
|
||||
|
||||
## 过滤规则
|
||||
|
||||
过滤器操作本质上是`SQL`里面的`WHERE`条件。
|
||||
|
||||
过滤器的规则,遵循了Python的ORM的规则,比如:
|
||||
|
||||
- [Tortoise ORM Filtering](https://tortoise.github.io/query.html#filtering)。
|
||||
- [Django Field lookups](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#field-lookups)
|
||||
|
||||
如果只是普通的查询,只需要传递`字段名`即可,但是如果需要一些特殊的查询,那么就需要加入`操作符`了。
|
||||
|
||||
特殊查询的语法规则其实很简单,就是使用双下划线`__`分割字段名和操作符:
|
||||
|
||||
```text
|
||||
{字段名}__{查找类型} : {值}
|
||||
{字段名}.{JSON字段名}__{查找类型} : {值}
|
||||
```
|
||||
|
||||
| 查找类型 | 示例 | SQL | 备注 |
|
||||
|-------------|---------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
|
||||
| not | `{"name__not" : "tom"}` | `WHERE NOT ("name" = "tom")` | |
|
||||
| in | `{"name__in" : "[\"tom\", \"jimmy\"]"}` | `WHERE name IN ("tom", "jimmy")` | |
|
||||
| not_in | `{"name__not_in" : "[\"tom\", \"jimmy\"]"}` | `WHERE name NOT IN ("tom", "jimmy")` | |
|
||||
| gte | `{"create_time__gte" : "2023-10-25"}` | `WHERE "create_time" >= "2023-10-25"` | |
|
||||
| gt | `{"create_time__gt" : "2023-10-25"}` | `WHERE "create_time" > "2023-10-25"` | |
|
||||
| lte | `{"create_time__lte" : "2023-10-25"}` | `WHERE "create_time" <= "2023-10-25"` | |
|
||||
| lt | `{"create_time__lt" : "2023-10-25"}` | `WHERE "create_time" < "2023-10-25"` | |
|
||||
| range | `{"create_time__range" : "[\"2023-10-25\", \"2024-10-25\"]"}` | `WHERE "create_time" BETWEEN "2023-10-25" AND "2024-10-25"` <br>或<br> `WHERE "create_time" >= "2023-10-25" AND "create_time" <= "2024-10-25"` | 需要注意的是: <br>1. 有些数据库的BETWEEN实现的开闭区间可能不一样。<br>2. 日期`2005-01-01`会被隐式转换为:`2005-01-01 00:00:00`,两个日期一致就会导致查询不到数据。 |
|
||||
| isnull | `{"name__isnull" : "True"}` | `WHERE name IS NULL` | |
|
||||
| not_isnull | `{"name__not_isnull" : "False"}` | `WHERE name IS NOT NULL` | |
|
||||
| contains | `{"name__contains" : "L"}` | `WHERE name LIKE '%L%';` | |
|
||||
| icontains | `{"name__icontains" : "L"}` | `WHERE name ILIKE '%L%';` | |
|
||||
| startswith | `{"name__startswith" : "La"}` | `WHERE name LIKE 'La%';` | |
|
||||
| istartswith | `{"name__istartswith" : "La"}` | `WHERE name ILIKE 'La%';` | |
|
||||
| endswith | `{"name__endswith" : "a"}` | `WHERE name LIKE '%a';` | |
|
||||
| iendswith | `{"name__iendswith" : "a"}` | `WHERE name ILIKE '%a';` | |
|
||||
| exact | `{"name__exact" : "a"}` | `WHERE name LIKE 'a';` | |
|
||||
| iexact | `{"name__iexact" : "a"}` | `WHERE name ILIKE 'a';` | |
|
||||
| regex | `{"title__regex" : "^(An?\|The) +"}` | MySQL: `WHERE title REGEXP BINARY '^(An?\|The) +'` <br> Oracle: `WHERE REGEXP_LIKE(title, '^(An?\|The) +', 'c');` <br> PostgreSQL: `WHERE title ~ '^(An?\|The) +';` <br> SQLite: `WHERE title REGEXP '^(An?\|The) +';` | |
|
||||
| iregex | `{"title__iregex" : "^(an?\|the) +"}` | MySQL: `WHERE title REGEXP '^(an?\|the) +'` <br> Oracle: `WHERE REGEXP_LIKE(title, '^(an?\|the) +', 'i');` <br> PostgreSQL: `WHERE title ~* '^(an?\|the) +';` <br> SQLite: `WHERE title REGEXP '(?i)^(an?\|the) +';` | |
|
||||
| search | | | |
|
||||
|
||||
以及将日期提取出来的查找类型:
|
||||
|
||||
| 查找类型 | 示例 | SQL | 备注 |
|
||||
|--------------|--------------------------------------|---------------------------------------------------|----------------------|
|
||||
| date | `{"pub_date__date" : "2023-01-01"}` | `WHERE DATE(pub_date) = '2023-01-01'` | |
|
||||
| year | `{"pub_date__year" : "2023"}` | `WHERE EXTRACT('YEAR' FROM pub_date) = '2023'` | 哪一年 |
|
||||
| iso_year | `{"pub_date__iso_year" : "2023"}` | `WHERE EXTRACT('ISOYEAR' FROM pub_date) = '2023'` | ISO 8601 一年中的周数 |
|
||||
| month | `{"pub_date__month" : "12"}` | `WHERE EXTRACT('MONTH' FROM pub_date) = '12'` | 月份,1-12 |
|
||||
| day | `{"pub_date__day" : "3"}` | `WHERE EXTRACT('DAY' FROM pub_date) = '3'` | 该月的某天(1-31) |
|
||||
| week | `{"pub_date__week" : "7"}` | `WHERE EXTRACT('WEEK' FROM pub_date) = '7'` | ISO 8601 周编号 一年中的周数 |
|
||||
| week_day | `{"pub_date__week_day" : "tom"}` | `` | 星期几 |
|
||||
| iso_week_day | `{"pub_date__iso_week_day" : "tom"}` | `` | |
|
||||
| quarter | `{"pub_date__quarter" : "1"}` | `WHERE EXTRACT('QUARTER' FROM pub_date) = '1'` | 一年中的季度 |
|
||||
| time | `{"pub_date__time" : "12:59:59"}` | `` | |
|
||||
| hour | `{"pub_date__hour" : "12"}` | `WHERE EXTRACT('HOUR' FROM pub_date) = '12'` | 小时(0-23) |
|
||||
| minute | `{"pub_date__minute" : "59"}` | `WHERE EXTRACT('MINUTE' FROM pub_date) = '59'` | 分钟 (0-59) |
|
||||
| second | `{"pub_date__second" : "59"}` | `WHERE EXTRACT('SECOND' FROM pub_date) = '59'` | 秒 (0-59) |
|
||||
180
query_parser/filter.go
Normal file
180
query_parser/filter.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package query_parser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/encoding"
|
||||
_ "github.com/go-kratos/kratos/v2/encoding/json"
|
||||
|
||||
"github.com/tx7do/go-utils/stringcase"
|
||||
)
|
||||
|
||||
type FilterOperator string
|
||||
|
||||
const (
|
||||
FilterNot = "not" // 不等于
|
||||
FilterIn = "in" // 检查值是否在列表中
|
||||
FilterNotIn = "not_in" // 不在列表中
|
||||
FilterGTE = "gte" // 大于或等于传递的值
|
||||
FilterGT = "gt" // 大于传递值
|
||||
FilterLTE = "lte" // 小于或等于传递值
|
||||
FilterLT = "lt" // 小于传递值
|
||||
FilterRange = "range" // 是否介于和给定的两个值之间
|
||||
FilterIsNull = "isnull" // 是否为空
|
||||
FilterNotIsNull = "not_isnull" // 是否不为空
|
||||
FilterContains = "contains" // 是否包含指定的子字符串
|
||||
FilterInsensitiveContains = "icontains" // 不区分大小写,是否包含指定的子字符串
|
||||
FilterStartsWith = "startswith" // 以值开头
|
||||
FilterInsensitiveStartsWith = "istartswith" // 不区分大小写,以值开头
|
||||
FilterEndsWith = "endswith" // 以值结尾
|
||||
FilterInsensitiveEndsWith = "iendswith" // 不区分大小写,以值结尾
|
||||
FilterExact = "exact" // 精确匹配
|
||||
FilterInsensitiveExact = "iexact" // 不区分大小写,精确匹配
|
||||
FilterRegex = "regex" // 正则表达式
|
||||
FilterInsensitiveRegex = "iregex" // 不区分大小写,正则表达式
|
||||
FilterSearch = "search" // 全文搜索
|
||||
)
|
||||
|
||||
const (
|
||||
DatePartDate = "date" // 日期
|
||||
DatePartYear = "year" // 年
|
||||
DatePartISOYear = "iso_year" // ISO8601 一年中的周数
|
||||
DatePartQuarter = "quarter" // 季度
|
||||
DatePartMonth = "month" // 月
|
||||
DatePartWeek = "week" // ISO8601 周编号 一年中的周数
|
||||
DatePartWeekDay = "week_day" // 星期几
|
||||
DatePartISOWeekDay = "iso_week_day" // ISO8601 星期几
|
||||
DatePartDay = "day" // 日
|
||||
DatePartTime = "time" // 小时:分钟:秒
|
||||
DatePartHour = "hour" // 小时
|
||||
DatePartMinute = "minute" // 分钟
|
||||
DatePartSecond = "second" // 秒
|
||||
DatePartMicrosecond = "microsecond" // 微秒
|
||||
)
|
||||
|
||||
const (
|
||||
JSONFilterFieldOperatorDelimiter = "__" // JSON过滤器 - 字段名和操作符的分隔符
|
||||
|
||||
QueryFilterFieldOperatorDelimiter = ":" // 自定义查询字符串过滤器 - 字段名和操作符的分隔符
|
||||
QueryFilterQueriesDelimiter = "," // 自定义查询字符串过滤器 - 多个键值对的分隔符
|
||||
QueryFilterValuesDelimiter = "|" // 自定义查询字符串过滤器 - 多个值的分隔符
|
||||
|
||||
JsonFieldDelimiter = "." // JSON字段分隔符
|
||||
)
|
||||
|
||||
type FilterHandler func(field, operator, value string)
|
||||
|
||||
// ParseFilterJSONString 解析过滤条件的JSON字符串,调用处理函数
|
||||
func ParseFilterJSONString(query string, handler FilterHandler) error {
|
||||
if query == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
codec := encoding.GetCodec("json")
|
||||
|
||||
var err error
|
||||
queryMap := make(map[string]string)
|
||||
if err = codec.Unmarshal([]byte(query), &queryMap); err == nil {
|
||||
for k, v := range queryMap {
|
||||
ParseFilterField(k, v, handler)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var queryMapArray []map[string]string
|
||||
if err = codec.Unmarshal([]byte(query), &queryMapArray); err == nil {
|
||||
for _, item := range queryMapArray {
|
||||
for k, v := range item {
|
||||
ParseFilterField(k, v, handler)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ParseFilterQueryString 解析过滤条件的查询字符串,调用处理函数
|
||||
func ParseFilterQueryString(query string, handler FilterHandler) error {
|
||||
if query == "" {
|
||||
return nil // 如果查询字符串为空,直接返回
|
||||
}
|
||||
|
||||
// 按逗号分割查询字符串,得到多个键值对
|
||||
pairs := SplitQueryQueries(query)
|
||||
for _, pair := range pairs {
|
||||
// 按冒号分割键值对,提取字段名和值
|
||||
parts := SplitQueryFieldAndOperator(pair)
|
||||
if len(parts) != 2 {
|
||||
continue // 跳过无效的键值对
|
||||
}
|
||||
|
||||
// 解码字段名
|
||||
key, err := DecodeSpecialCharacters(strings.TrimSpace(parts[0]))
|
||||
if err != nil {
|
||||
continue // 跳过解码失败的键值对
|
||||
}
|
||||
|
||||
// 解码字段值
|
||||
value, err := DecodeSpecialCharacters(strings.TrimSpace(parts[1]))
|
||||
if err != nil {
|
||||
continue // 跳过解码失败的键值对
|
||||
}
|
||||
|
||||
// 调用 ParseFilterField 解析字段和操作符
|
||||
ParseFilterField(key, value, handler)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseFilterField 解析过滤条件字符串,调用处理函数
|
||||
func ParseFilterField(key, value string, handler FilterHandler) {
|
||||
if key == "" || value == "" {
|
||||
return // 没有过滤条件
|
||||
}
|
||||
|
||||
// 处理字段和操作符
|
||||
parts := SplitJsonFieldAndOperator(key)
|
||||
if len(parts) < 1 {
|
||||
return // 无效的字段格式
|
||||
}
|
||||
|
||||
field := strings.TrimSpace(parts[0])
|
||||
if field == "" {
|
||||
return
|
||||
}
|
||||
field = stringcase.ToSnakeCase(parts[0])
|
||||
|
||||
op := ""
|
||||
if len(parts) > 1 {
|
||||
op = parts[1]
|
||||
}
|
||||
|
||||
handler(field, op, value)
|
||||
}
|
||||
|
||||
// SplitJsonFieldAndOperator JSON过滤器 - 分割“字段名”和“操作符”
|
||||
func SplitJsonFieldAndOperator(field string) []string {
|
||||
return strings.Split(field, JSONFilterFieldOperatorDelimiter)
|
||||
}
|
||||
|
||||
// SplitQueryFieldAndOperator 自定义查询字符串过滤器 - 分割“字段名”和“操作符”
|
||||
func SplitQueryFieldAndOperator(field string) []string {
|
||||
return strings.Split(field, QueryFilterFieldOperatorDelimiter)
|
||||
}
|
||||
|
||||
// SplitQueryQueries 自定义查询字符串过滤器 - 分割多个键值对
|
||||
func SplitQueryQueries(field string) []string {
|
||||
return strings.Split(field, QueryFilterQueriesDelimiter)
|
||||
}
|
||||
|
||||
// SplitQueryValues 自定义查询字符串过滤器 - 分割多个值
|
||||
func SplitQueryValues(field string) []string {
|
||||
return strings.Split(field, QueryFilterValuesDelimiter)
|
||||
}
|
||||
|
||||
// SplitJSONField 将JSONB字段字符串按分隔符分割成多个字段
|
||||
func SplitJSONField(field string) []string {
|
||||
return strings.Split(field, JsonFieldDelimiter)
|
||||
}
|
||||
401
query_parser/filter_test.go
Normal file
401
query_parser/filter_test.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package query_parser
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseFilterJSONString(t *testing.T) {
|
||||
var results []struct {
|
||||
Field string
|
||||
Operator string
|
||||
Value string
|
||||
}
|
||||
|
||||
handler := func(field, operator, value string) {
|
||||
results = append(results, struct {
|
||||
Field string
|
||||
Operator string
|
||||
Value string
|
||||
}{Field: field, Operator: operator, Value: value})
|
||||
}
|
||||
|
||||
// 测试解析单个过滤条件
|
||||
results = nil
|
||||
err := ParseFilterJSONString(`{"name__exact":"John"}`, handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.Equal(t, "exact", results[0].Operator)
|
||||
assert.Equal(t, "John", results[0].Value)
|
||||
|
||||
// 测试解析多个过滤条件
|
||||
results = nil
|
||||
err = ParseFilterJSONString(`[{"age__gte":"30"},{"status__exact":"active"}]`, handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(results))
|
||||
assert.Equal(t, "age", results[0].Field)
|
||||
assert.Equal(t, "gte", results[0].Operator)
|
||||
assert.Equal(t, "30", results[0].Value)
|
||||
assert.Equal(t, "status", results[1].Field)
|
||||
assert.Equal(t, "exact", results[1].Operator)
|
||||
assert.Equal(t, "active", results[1].Value)
|
||||
|
||||
// 测试空字符串
|
||||
results = nil
|
||||
err = ParseFilterJSONString("", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试无效的JSON字符串
|
||||
results = nil
|
||||
err = ParseFilterJSONString(`invalid_json`, handler)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试包含特殊字符的字段和值
|
||||
results = nil
|
||||
err = ParseFilterJSONString(`{"na!me__exact":"Jo@hn"}`, handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "na_me", results[0].Field)
|
||||
assert.Equal(t, "exact", results[0].Operator)
|
||||
assert.Equal(t, "Jo@hn", results[0].Value)
|
||||
|
||||
// 测试包含特殊字符的多个过滤条件
|
||||
results = nil
|
||||
err = ParseFilterJSONString(`[{"ag#e__gte":"30"},{"sta$tus__exact":"ac^tive"}]`, handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(results))
|
||||
assert.Equal(t, "ag_e", results[0].Field)
|
||||
assert.Equal(t, "gte", results[0].Operator)
|
||||
assert.Equal(t, "30", results[0].Value)
|
||||
assert.Equal(t, "sta_tus", results[1].Field)
|
||||
assert.Equal(t, "exact", results[1].Operator)
|
||||
assert.Equal(t, "ac^tive", results[1].Value)
|
||||
|
||||
// 测试包含特殊字符的无效 JSON 字符串
|
||||
results = nil
|
||||
err = ParseFilterJSONString(`{"na!me__exact":Jo@hn}`, handler)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
}
|
||||
|
||||
func TestParseFilterQueryString(t *testing.T) {
|
||||
var results []struct {
|
||||
Field string
|
||||
Operator string
|
||||
Value string
|
||||
}
|
||||
|
||||
handler := func(field, operator, value string) {
|
||||
results = append(results, struct {
|
||||
Field string
|
||||
Operator string
|
||||
Value string
|
||||
}{Field: field, Operator: operator, Value: value})
|
||||
}
|
||||
|
||||
// 测试解析单个过滤条件
|
||||
results = nil
|
||||
err := ParseFilterQueryString("name__exact:John", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.Equal(t, "exact", results[0].Operator)
|
||||
assert.Equal(t, "John", results[0].Value)
|
||||
|
||||
// 测试解析多个过滤条件
|
||||
results = nil
|
||||
err = ParseFilterQueryString("age__gte:30,status__exact:active", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(results))
|
||||
assert.Equal(t, "age", results[0].Field)
|
||||
assert.Equal(t, "gte", results[0].Operator)
|
||||
assert.Equal(t, "30", results[0].Value)
|
||||
assert.Equal(t, "status", results[1].Field)
|
||||
assert.Equal(t, "exact", results[1].Operator)
|
||||
assert.Equal(t, "active", results[1].Value)
|
||||
|
||||
// 测试空字符串
|
||||
results = nil
|
||||
err = ParseFilterQueryString("", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试无效的查询字符串
|
||||
results = nil
|
||||
err = ParseFilterQueryString("invalid_query", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试包含特殊字符的字段和值
|
||||
results = nil
|
||||
err = ParseFilterQueryString("na!me__exact:Jo@hn", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "na_me", results[0].Field)
|
||||
assert.Equal(t, "exact", results[0].Operator)
|
||||
assert.Equal(t, "Jo@hn", results[0].Value)
|
||||
|
||||
// 测试包含特殊字符的多个过滤条件
|
||||
results = nil
|
||||
err = ParseFilterQueryString("ag#e__gte:30,sta$tus__exact:ac^tive", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(results))
|
||||
assert.Equal(t, "ag_e", results[0].Field)
|
||||
assert.Equal(t, "gte", results[0].Operator)
|
||||
assert.Equal(t, "30", results[0].Value)
|
||||
assert.Equal(t, "sta_tus", results[1].Field)
|
||||
assert.Equal(t, "exact", results[1].Operator)
|
||||
assert.Equal(t, "ac^tive", results[1].Value)
|
||||
|
||||
// 测试 Field 中包含分隔符
|
||||
results = nil
|
||||
err = ParseFilterQueryString("na:me__exact:John", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试 Operator 中包含分隔符
|
||||
results = nil
|
||||
err = ParseFilterQueryString("name__ex:act:John", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试 Value 中包含分隔符
|
||||
results = nil
|
||||
err = ParseFilterQueryString("name__exact:Jo|hn", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.Equal(t, "exact", results[0].Operator)
|
||||
assert.Equal(t, "Jo|hn", results[0].Value)
|
||||
|
||||
// 测试多个过滤条件中包含分隔符
|
||||
results = nil
|
||||
err = ParseFilterQueryString("ag:e__gte:30,sta|tus__exact:ac|tive", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(results))
|
||||
|
||||
// 测试 Field 中包含编码后的分隔符
|
||||
results = nil
|
||||
encodedField := EncodeSpecialCharacters("na:me")
|
||||
err = ParseFilterQueryString(encodedField+"__exact:John", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "na_me", results[0].Field) // 注意:这里的字段名会被转换为 snake_case
|
||||
assert.Equal(t, "exact", results[0].Operator)
|
||||
assert.Equal(t, "John", results[0].Value)
|
||||
|
||||
// 测试 Operator 中包含编码后的分隔符
|
||||
results = nil
|
||||
encodedOperator := EncodeSpecialCharacters("ex:act")
|
||||
err = ParseFilterQueryString("name__"+encodedOperator+":John", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.Equal(t, "ex:act", results[0].Operator)
|
||||
assert.Equal(t, "John", results[0].Value)
|
||||
|
||||
// 测试 Value 中包含编码后的分隔符
|
||||
results = nil
|
||||
encodedValue := EncodeSpecialCharacters("Jo|hn")
|
||||
err = ParseFilterQueryString("name__exact:"+encodedValue, handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.Equal(t, "exact", results[0].Operator)
|
||||
assert.Equal(t, "Jo|hn", results[0].Value)
|
||||
|
||||
// 测试多个过滤条件中包含编码后的分隔符
|
||||
results = nil
|
||||
encodedField1 := EncodeSpecialCharacters("ag:e")
|
||||
encodedValue1 := EncodeSpecialCharacters("30")
|
||||
encodedField2 := EncodeSpecialCharacters("sta|tus")
|
||||
encodedValue2 := EncodeSpecialCharacters("ac|tive")
|
||||
err = ParseFilterQueryString(encodedField1+"__gte:"+encodedValue1+","+encodedField2+"__exact:"+encodedValue2, handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(results))
|
||||
assert.Equal(t, "ag_e", results[0].Field) // 注意:这里的字段名会被转换为 snake_case
|
||||
assert.Equal(t, "gte", results[0].Operator)
|
||||
assert.Equal(t, "30", results[0].Value)
|
||||
assert.Equal(t, "sta_tus", results[1].Field) // 注意:这里的字段名会被转换为 snake_case
|
||||
assert.Equal(t, "exact", results[1].Operator)
|
||||
assert.Equal(t, "ac|tive", results[1].Value)
|
||||
}
|
||||
|
||||
func TestParseFilterField(t *testing.T) {
|
||||
var results []struct {
|
||||
Field string
|
||||
Operator string
|
||||
Value string
|
||||
}
|
||||
|
||||
handler := func(field, operator, value string) {
|
||||
results = append(results, struct {
|
||||
Field string
|
||||
Operator string
|
||||
Value string
|
||||
}{Field: field, Operator: operator, Value: value})
|
||||
}
|
||||
|
||||
// 测试正常解析
|
||||
results = nil
|
||||
ParseFilterField("name__exact", "John", handler)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.Equal(t, "exact", results[0].Operator)
|
||||
assert.Equal(t, "John", results[0].Value)
|
||||
|
||||
// 测试无操作符解析
|
||||
results = nil
|
||||
ParseFilterField("name", "John", handler)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.Equal(t, "", results[0].Operator)
|
||||
assert.Equal(t, "John", results[0].Value)
|
||||
|
||||
// 测试空字段
|
||||
results = nil
|
||||
ParseFilterField("", "John", handler)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试空值
|
||||
results = nil
|
||||
ParseFilterField("name__exact", "", handler)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试无效字段格式
|
||||
results = nil
|
||||
ParseFilterField("__exact", "John", handler)
|
||||
assert.Equal(t, 0, len(results))
|
||||
}
|
||||
|
||||
func TestSplitJsonFieldAndOperator(t *testing.T) {
|
||||
// 测试正常分割
|
||||
result := SplitJsonFieldAndOperator("name__exact")
|
||||
assert.Equal(t, 2, len(result))
|
||||
assert.Equal(t, "name", result[0])
|
||||
assert.Equal(t, "exact", result[1])
|
||||
|
||||
// 测试无操作符
|
||||
result = SplitJsonFieldAndOperator("name")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "name", result[0])
|
||||
|
||||
// 测试空字符串
|
||||
result = SplitJsonFieldAndOperator("")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "", result[0])
|
||||
|
||||
// 测试多个分隔符
|
||||
result = SplitJsonFieldAndOperator("name__exact__extra")
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "name", result[0])
|
||||
assert.Equal(t, "exact", result[1])
|
||||
assert.Equal(t, "extra", result[2])
|
||||
}
|
||||
|
||||
func TestSplitJSONField(t *testing.T) {
|
||||
// 测试正常分割
|
||||
result := SplitJSONField("user.address.city")
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "user", result[0])
|
||||
assert.Equal(t, "address", result[1])
|
||||
assert.Equal(t, "city", result[2])
|
||||
|
||||
// 测试单个字段
|
||||
result = SplitJSONField("user")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "user", result[0])
|
||||
|
||||
// 测试空字符串
|
||||
result = SplitJSONField("")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "", result[0])
|
||||
|
||||
// 测试多个分隔符
|
||||
result = SplitJSONField("user..address.city")
|
||||
assert.Equal(t, 4, len(result))
|
||||
assert.Equal(t, "user", result[0])
|
||||
assert.Equal(t, "", result[1])
|
||||
assert.Equal(t, "address", result[2])
|
||||
assert.Equal(t, "city", result[3])
|
||||
}
|
||||
|
||||
func TestSplitQueryFieldAndOperator(t *testing.T) {
|
||||
// 测试正常分割
|
||||
result := SplitQueryFieldAndOperator("name:exact")
|
||||
assert.Equal(t, 2, len(result))
|
||||
assert.Equal(t, "name", result[0])
|
||||
assert.Equal(t, "exact", result[1])
|
||||
|
||||
// 测试无操作符
|
||||
result = SplitQueryFieldAndOperator("name")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "name", result[0])
|
||||
|
||||
// 测试空字符串
|
||||
result = SplitQueryFieldAndOperator("")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "", result[0])
|
||||
|
||||
// 测试多个分隔符
|
||||
result = SplitQueryFieldAndOperator("name:exact:extra")
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "name", result[0])
|
||||
assert.Equal(t, "exact", result[1])
|
||||
assert.Equal(t, "extra", result[2])
|
||||
}
|
||||
|
||||
func TestSplitQueryQueries(t *testing.T) {
|
||||
// 测试正常分割多个键值对
|
||||
result := SplitQueryQueries("name:John,age:30,status:active")
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "name:John", result[0])
|
||||
assert.Equal(t, "age:30", result[1])
|
||||
assert.Equal(t, "status:active", result[2])
|
||||
|
||||
// 测试单个键值对
|
||||
result = SplitQueryQueries("name:John")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "name:John", result[0])
|
||||
|
||||
// 测试空字符串
|
||||
result = SplitQueryQueries("")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "", result[0])
|
||||
|
||||
// 测试多个分隔符
|
||||
result = SplitQueryQueries("name:John,,age:30")
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "name:John", result[0])
|
||||
assert.Equal(t, "", result[1])
|
||||
assert.Equal(t, "age:30", result[2])
|
||||
}
|
||||
|
||||
func TestSplitQueryValues(t *testing.T) {
|
||||
// 测试正常分割多个值
|
||||
result := SplitQueryValues("value1|value2|value3")
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "value1", result[0])
|
||||
assert.Equal(t, "value2", result[1])
|
||||
assert.Equal(t, "value3", result[2])
|
||||
|
||||
// 测试单个值
|
||||
result = SplitQueryValues("value1")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "value1", result[0])
|
||||
|
||||
// 测试空字符串
|
||||
result = SplitQueryValues("")
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "", result[0])
|
||||
|
||||
// 测试多个分隔符
|
||||
result = SplitQueryValues("value1||value2")
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "value1", result[0])
|
||||
assert.Equal(t, "", result[1])
|
||||
assert.Equal(t, "value2", result[2])
|
||||
}
|
||||
21
query_parser/go.mod
Normal file
21
query_parser/go.mod
Normal file
@@ -0,0 +1,21 @@
|
||||
module github.com/tx7do/go-utils/query_parser
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.2
|
||||
|
||||
require (
|
||||
github.com/go-kratos/kratos/v2 v2.8.4
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tx7do/go-utils v1.1.29
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/tx7do/go-utils => ../
|
||||
26
query_parser/go.sum
Normal file
26
query_parser/go.sum
Normal file
@@ -0,0 +1,26 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs=
|
||||
github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYqSNKwGFiiRcw=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
50
query_parser/orderby.go
Normal file
50
query_parser/orderby.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package query_parser
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OrderByHandler func(field string, desc bool)
|
||||
|
||||
// ParseOrderByString 解析排序字符串,调用处理函数。
|
||||
func ParseOrderByString(orderBy string, handler OrderByHandler) error {
|
||||
if orderBy == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.Split(orderBy, ",")
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
ParseOrderByField(part, handler)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseOrderByStrings 解析多个排序字符串,调用处理函数。
|
||||
func ParseOrderByStrings(orderBys []string, handler OrderByHandler) error {
|
||||
for _, v := range orderBys {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
ParseOrderByField(v, handler)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseOrderByField 解析单个排序字段,调用处理函数。
|
||||
func ParseOrderByField(orderBy string, handler OrderByHandler) {
|
||||
orderBy = strings.TrimSpace(orderBy)
|
||||
if orderBy == "" {
|
||||
return // 没有排序条件
|
||||
}
|
||||
|
||||
if strings.HasPrefix(orderBy, "-") {
|
||||
handler(orderBy[1:], true) // 降序
|
||||
} else if strings.HasPrefix(orderBy, "+") {
|
||||
handler(orderBy[1:], false) // 升序
|
||||
} else {
|
||||
handler(orderBy, false) // 升序
|
||||
}
|
||||
}
|
||||
126
query_parser/orderby_test.go
Normal file
126
query_parser/orderby_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package query_parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseOrderByString(t *testing.T) {
|
||||
var results []struct {
|
||||
Field string
|
||||
Desc bool
|
||||
}
|
||||
|
||||
handler := func(field string, desc bool) {
|
||||
results = append(results, struct {
|
||||
Field string
|
||||
Desc bool
|
||||
}{Field: field, Desc: desc})
|
||||
}
|
||||
|
||||
// 测试正常解析
|
||||
err := ParseOrderByString("name,-age,+created_at", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.False(t, results[0].Desc)
|
||||
assert.Equal(t, "age", results[1].Field)
|
||||
assert.True(t, results[1].Desc)
|
||||
assert.Equal(t, "created_at", results[2].Field)
|
||||
assert.False(t, results[2].Desc)
|
||||
|
||||
// 测试空字符串
|
||||
results = nil
|
||||
err = ParseOrderByString("", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试只有空格的字符串
|
||||
results = nil
|
||||
err = ParseOrderByString(" ", handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
}
|
||||
|
||||
func TestParseOrderByStrings(t *testing.T) {
|
||||
var results []struct {
|
||||
Field string
|
||||
Desc bool
|
||||
}
|
||||
|
||||
handler := func(field string, desc bool) {
|
||||
results = append(results, struct {
|
||||
Field string
|
||||
Desc bool
|
||||
}{Field: field, Desc: desc})
|
||||
}
|
||||
|
||||
// 测试正常解析
|
||||
err := ParseOrderByStrings([]string{"name", "-age", "+created_at"}, handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.False(t, results[0].Desc)
|
||||
assert.Equal(t, "age", results[1].Field)
|
||||
assert.True(t, results[1].Desc)
|
||||
assert.Equal(t, "created_at", results[2].Field)
|
||||
assert.False(t, results[2].Desc)
|
||||
|
||||
// 测试空字符串数组
|
||||
results = nil
|
||||
err = ParseOrderByStrings([]string{}, handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试包含空字符串的数组
|
||||
results = nil
|
||||
err = ParseOrderByStrings([]string{"", " "}, handler)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(results))
|
||||
}
|
||||
|
||||
func TestParseOrderByField(t *testing.T) {
|
||||
var results []struct {
|
||||
Field string
|
||||
Desc bool
|
||||
}
|
||||
|
||||
handler := func(field string, desc bool) {
|
||||
results = append(results, struct {
|
||||
Field string
|
||||
Desc bool
|
||||
}{Field: field, Desc: desc})
|
||||
}
|
||||
|
||||
// 测试升序解析
|
||||
results = nil
|
||||
ParseOrderByField("name", handler)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "name", results[0].Field)
|
||||
assert.False(t, results[0].Desc)
|
||||
|
||||
// 测试降序解析
|
||||
results = nil
|
||||
ParseOrderByField("-age", handler)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "age", results[0].Field)
|
||||
assert.True(t, results[0].Desc)
|
||||
|
||||
// 测试带+的升序解析
|
||||
results = nil
|
||||
ParseOrderByField("+created_at", handler)
|
||||
assert.Equal(t, 1, len(results))
|
||||
assert.Equal(t, "created_at", results[0].Field)
|
||||
assert.False(t, results[0].Desc)
|
||||
|
||||
// 测试空字符串
|
||||
results = nil
|
||||
ParseOrderByField("", handler)
|
||||
assert.Equal(t, 0, len(results))
|
||||
|
||||
// 测试只有空格的字符串
|
||||
results = nil
|
||||
ParseOrderByField(" ", handler)
|
||||
assert.Equal(t, 0, len(results))
|
||||
}
|
||||
13
query_parser/utils.go
Normal file
13
query_parser/utils.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package query_parser
|
||||
|
||||
import "net/url"
|
||||
|
||||
// EncodeSpecialCharacters 对字符串进行编码
|
||||
func EncodeSpecialCharacters(input string) string {
|
||||
return url.QueryEscape(input)
|
||||
}
|
||||
|
||||
// DecodeSpecialCharacters 对字符串进行解码
|
||||
func DecodeSpecialCharacters(input string) (string, error) {
|
||||
return url.QueryUnescape(input)
|
||||
}
|
||||
80
stringcase/README.md
Normal file
80
stringcase/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 命名法转换
|
||||
|
||||
- camelCase 驼峰式命名法(大驼峰)
|
||||
- PascalCase 帕斯卡命名法(小驼峰)
|
||||
- snake_case 蛇形命名法
|
||||
- kebab-case 烤肉串命名法
|
||||
|
||||
## camelCase 驼峰式命名法(大驼峰)
|
||||
|
||||
驼峰式命名法(Camel case)是一种不使用空格,将多个单词连起来形成一个标识符的命名方式,其中每个单词的首字母(除了第一个单词,如果使用小驼峰式命名法)都大写,就像骆驼的驼峰一样。
|
||||
|
||||
驼峰式命名法分为两种:首字母小写的“小驼峰式”(lowerCamelCase)和首字母大写的“大驼峰式”(UpperCamelCase,也称为帕斯卡命名法PascalCase)。
|
||||
|
||||
- **小驼峰式(lowerCamelCase)**: 第一个单词的首字母小写,后续单词的首字母大写。例如:`myVariableName`。
|
||||
- **大驼峰式(UpperCamelCase)**: 每个单词的首字母都大写。例如:MyVariableName,也称为帕斯卡命名法(PascalCase)。
|
||||
|
||||
在 JavaScript、Java和C#中,驼峰式大小写常用于变量和函数的命名。
|
||||
|
||||
```javascript
|
||||
let firstName = "John";
|
||||
let lastName = "Doe";
|
||||
|
||||
function printFullName(firstName, lastName) {
|
||||
let fullName = firstName + " " + lastName;
|
||||
console.log(fullName);
|
||||
}
|
||||
```
|
||||
|
||||
## PascalCase 帕斯卡命名法(小驼峰)
|
||||
|
||||
`PascalCase`,也称为`UpperCamelCase`,是一种在编程中使用的命名约定。它要求每个单词(包括第一个单词)的首字母都大写,并且单词之间没有空格或分隔符(如
|
||||
`_`)。例如,`ThisIsPascalCase`,`MyClassName` 都是使用PascalCase的例子。
|
||||
|
||||
PascalCase 通常用于在 C#、Java 和TypeScript等语言中命名类、接口和其他类型。
|
||||
|
||||
```typescript
|
||||
class Person {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
|
||||
constructor(firstName: string, lastName: string) {
|
||||
this.firstName = firstName;
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
printFullName(): void {
|
||||
let fullName = this.firstName + " " + this.lastName;
|
||||
console.log(fullName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## snake_case 蛇形命名法
|
||||
|
||||
蛇形命名法是一种使用下划线 (`_`) 分隔单词的命名方式。之所以叫蛇形命名法,是因为"`snake_case`"的
|
||||
下划线的形状类似于蛇腹上的鳞片。蛇形命名法通常用于Python、Ruby 和JavaScript等语言的变量名和函数名。
|
||||
|
||||
```python
|
||||
first_name = "John"
|
||||
last_name = "Doe"
|
||||
|
||||
def print_full_name(first_name, last_name):
|
||||
full_name = first_name + " " + last_name
|
||||
print(full_name)
|
||||
```
|
||||
|
||||
## kebab-case 烤肉串命名法
|
||||
|
||||
`kebab-case(烤肉串命名法)`,也被称作 `kebab case`、`dash-case(破折号式)`、`hyphen-case(连字符式)`、`lisp-case(Lisp 式)`。
|
||||
|
||||
kebab-case 要求短语内的各个单词或缩写之间以`-`(连字符)做间隔。 例如:"`kebab-case`"。
|
||||
|
||||
短横线命名法通常用于 URL、文件名和 HTML/CSS 类名。
|
||||
|
||||
```html
|
||||
|
||||
<div class="user-profile">
|
||||
<p>This is a user profile.</p>
|
||||
</div>
|
||||
```
|
||||
65
stringcase/camel_case.go
Normal file
65
stringcase/camel_case.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func UpperCamelCase(input string) string {
|
||||
return camelCase(input, true)
|
||||
}
|
||||
|
||||
func LowerCamelCase(input string) string {
|
||||
return camelCase(input, false)
|
||||
}
|
||||
|
||||
// ToPascalCase 把字符转换为 帕斯卡命名/大驼峰命名法(CamelCase)
|
||||
func ToPascalCase(input string) string {
|
||||
return camelCase(input, true)
|
||||
}
|
||||
|
||||
func camelCase(input string, upper bool) string {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return input
|
||||
}
|
||||
|
||||
// 分割字符串
|
||||
words := Split(input)
|
||||
if len(words) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
filteredWords := make([]string, 0, len(words))
|
||||
for _, word := range words {
|
||||
if strings.TrimSpace(word) != "" {
|
||||
filteredWords = append(filteredWords, word)
|
||||
}
|
||||
}
|
||||
words = filteredWords
|
||||
if len(words) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for i, word := range words {
|
||||
if word == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
runes := []rune(word)
|
||||
if len(runes) > 0 {
|
||||
if i == 0 && !upper {
|
||||
runes[0] = unicode.ToLower(runes[0]) // LowerCamelCase首单词首字母小写
|
||||
} else {
|
||||
runes[0] = unicode.ToUpper(runes[0]) // UpperCamelCase或后续单词首字母大写
|
||||
}
|
||||
for j := 1; j < len(runes); j++ {
|
||||
runes[j] = unicode.ToLower(runes[j]) // 其余字母统一小写
|
||||
}
|
||||
words[i] = string(runes)
|
||||
}
|
||||
}
|
||||
|
||||
// 合并结果
|
||||
return strings.Join(words, "")
|
||||
}
|
||||
111
stringcase/camel_case_test.go
Normal file
111
stringcase/camel_case_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUpperCamelCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"hello world", "HelloWorld"},
|
||||
{"hello_world", "HelloWorld"},
|
||||
{"hello-world", "HelloWorld"},
|
||||
{"hello.world", "HelloWorld"},
|
||||
{"helloWorld", "HelloWorld"},
|
||||
{"HelloWorld", "HelloWorld"},
|
||||
{"HTTPStatusCode", "HttpStatusCode"},
|
||||
{"ParseURL.DoParse", "ParseUrlDoParse"},
|
||||
{"ParseUrl.DoParse", "ParseUrlDoParse"},
|
||||
{"parse_url.do_parse", "ParseUrlDoParse"},
|
||||
{"convert space", "ConvertSpace"},
|
||||
{"convert-dash", "ConvertDash"},
|
||||
{"skip___multiple_underscores", "SkipMultipleUnderscores"},
|
||||
{"skip multiple spaces", "SkipMultipleSpaces"},
|
||||
{"skip---multiple-dashes", "SkipMultipleDashes"},
|
||||
{"", ""},
|
||||
{"a", "A"},
|
||||
{"Z", "Z"},
|
||||
{"special-characters_test", "SpecialCharactersTest"},
|
||||
{"numbers123test", "Numbers123Test"},
|
||||
{"hello world!", "HelloWorld"},
|
||||
{"test@with#symbols", "TestWithSymbols"},
|
||||
{"complexCase123!@#", "ComplexCase123"},
|
||||
|
||||
{"snake_case_string", "SnakeCaseString"},
|
||||
{"kebab-case-string", "KebabCaseString"},
|
||||
{"PascalCaseString", "PascalCaseString"},
|
||||
{"camelCaseString", "CamelCaseString"},
|
||||
{"HTTPRequest", "HttpRequest"},
|
||||
{"user ID", "UserId"},
|
||||
{"UserId", "UserId"},
|
||||
{"userID", "UserId"},
|
||||
{"UserID", "UserId"},
|
||||
{"123NumberPrefix", "123NumberPrefix"},
|
||||
{"__leading_underscores", "LeadingUnderscores"},
|
||||
{"trailing_underscores__", "TrailingUnderscores"},
|
||||
{"multiple___underscores", "MultipleUnderscores"},
|
||||
{" spaces around ", "SpacesAround"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := UpperCamelCase(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("UpperCamelCase(%q) = %q; expected %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowerCamelCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"hello world", "helloWorld"},
|
||||
{"hello_world", "helloWorld"},
|
||||
{"hello-world", "helloWorld"},
|
||||
{"hello.world", "helloWorld"},
|
||||
{"helloWorld", "helloWorld"},
|
||||
{"HelloWorld", "helloWorld"},
|
||||
{"HTTPStatusCode", "httpStatusCode"},
|
||||
{"ParseURL.DoParse", "parseUrlDoParse"},
|
||||
{"ParseUrl.DoParse", "parseUrlDoParse"},
|
||||
{"parse_url.do_parse", "parseUrlDoParse"},
|
||||
{"convert space", "convertSpace"},
|
||||
{"convert-dash", "convertDash"},
|
||||
{"skip___multiple_underscores", "skipMultipleUnderscores"},
|
||||
{"skip multiple spaces", "skipMultipleSpaces"},
|
||||
{"skip---multiple-dashes", "skipMultipleDashes"},
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"Z", "z"},
|
||||
{"special-characters_test", "specialCharactersTest"},
|
||||
{"numbers123test", "numbers123Test"},
|
||||
{"hello world!", "helloWorld"},
|
||||
{"test@with#symbols", "testWithSymbols"},
|
||||
{"complexCase123!@#", "complexCase123"},
|
||||
|
||||
{"snake_case_string", "snakeCaseString"},
|
||||
{"kebab-case-string", "kebabCaseString"},
|
||||
{"PascalCaseString", "pascalCaseString"},
|
||||
{"camelCaseString", "camelCaseString"},
|
||||
{"HTTPRequest", "httpRequest"},
|
||||
{"user ID", "userId"},
|
||||
{"UserId", "userId"},
|
||||
{"userID", "userId"},
|
||||
{"UserID", "userId"},
|
||||
{"123NumberPrefix", "123NumberPrefix"},
|
||||
{"__leading_underscores", "leadingUnderscores"},
|
||||
{"trailing_underscores__", "trailingUnderscores"},
|
||||
{"multiple___underscores", "multipleUnderscores"},
|
||||
{" spaces around ", "spacesAround"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := LowerCamelCase(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("LowerCamelCase(%q) = %q; expected %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
9
stringcase/kebab_case.go
Normal file
9
stringcase/kebab_case.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package stringcase
|
||||
|
||||
func KebabCase(s string) string {
|
||||
return delimiterCase(s, '-', false)
|
||||
}
|
||||
|
||||
func UpperKebabCase(s string) string {
|
||||
return delimiterCase(s, '-', true)
|
||||
}
|
||||
49
stringcase/kebab_case_test.go
Normal file
49
stringcase/kebab_case_test.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package stringcase
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestKebabCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"HelloWorld", "hello-world"},
|
||||
{"helloWorld", "hello-world"},
|
||||
{"Hello World", "hello-world"},
|
||||
{"hello world!", "hello-world"},
|
||||
{"Numbers123Test", "numbers-123-test"},
|
||||
{"", ""},
|
||||
{"_", ""},
|
||||
{"__Hello__World__", "hello-world"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := KebabCase(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("KebabCase(%q) = %q; expected %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpperKebabCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"HelloWorld", "HELLO-WORLD"},
|
||||
{"helloWorld", "HELLO-WORLD"},
|
||||
{"Hello World", "HELLO-WORLD"},
|
||||
{"hello world!", "HELLO-WORLD"},
|
||||
{"Numbers123Test", "NUMBERS-123-TEST"},
|
||||
{"", ""},
|
||||
{"_", ""},
|
||||
{"__Hello__World__", "HELLO-WORLD"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := UpperKebabCase(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("UpperKebabCase(%q) = %q; expected %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
50
stringcase/snake_case.go
Normal file
50
stringcase/snake_case.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ToSnakeCase 把字符转换为 蛇形命名法(snake_case)
|
||||
func ToSnakeCase(input string) string {
|
||||
return SnakeCase(input)
|
||||
}
|
||||
|
||||
func SnakeCase(s string) string {
|
||||
return delimiterCase(s, '_', false)
|
||||
}
|
||||
|
||||
func UpperSnakeCase(s string) string {
|
||||
return delimiterCase(s, '_', true)
|
||||
}
|
||||
|
||||
func delimiterCase(input string, delimiter rune, upperCase bool) string {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return input
|
||||
}
|
||||
|
||||
// 使用 Split 分割字符串
|
||||
words := Split(input)
|
||||
filteredWords := make([]string, 0, len(words))
|
||||
for _, word := range words {
|
||||
if strings.TrimSpace(word) != "" {
|
||||
filteredWords = append(filteredWords, word)
|
||||
}
|
||||
}
|
||||
|
||||
adjustCase := toLower
|
||||
if upperCase {
|
||||
adjustCase = toUpper
|
||||
}
|
||||
|
||||
for i, word := range filteredWords {
|
||||
runes := []rune(word)
|
||||
for j := 0; j < len(runes); j++ {
|
||||
runes[j] = adjustCase(runes[j])
|
||||
}
|
||||
filteredWords[i] = string(runes)
|
||||
}
|
||||
|
||||
// 使用分隔符连接结果
|
||||
return strings.Join(filteredWords, string(delimiter))
|
||||
}
|
||||
85
stringcase/snake_case_test.go
Normal file
85
stringcase/snake_case_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToSnakeCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"snake_case", "snake_case"},
|
||||
{"CamelCase", "camel_case"},
|
||||
{"lowerCamelCase", "lower_camel_case"},
|
||||
{"F", "f"},
|
||||
{"Foo", "foo"},
|
||||
{"FooB", "foo_b"},
|
||||
{"FooID", "foo_id"},
|
||||
{" FooBar\t", "foo_bar"},
|
||||
{"HTTPStatusCode", "http_status_code"},
|
||||
{"ParseURL.DoParse", "parse_url_do_parse"},
|
||||
{"Convert Space", "convert_space"},
|
||||
{"Convert-dash", "convert_dash"},
|
||||
{"Skip___MultipleUnderscores", "skip_multiple_underscores"},
|
||||
{"Skip MultipleSpaces", "skip_multiple_spaces"},
|
||||
{"Skip---MultipleDashes", "skip_multiple_dashes"},
|
||||
{"Hello World", "hello_world"},
|
||||
{"Multiple Words Example", "multiple_words_example"},
|
||||
{"", ""},
|
||||
{"A", "a"},
|
||||
{"z", "z"},
|
||||
{"Special-Characters_Test", "special_characters_test"},
|
||||
{"Numbers123Test", "numbers_123_test"},
|
||||
{"Hello World!", "hello_world"},
|
||||
{"Test@With#Symbols", "test_with_symbols"},
|
||||
{"ComplexCase123!@#", "complex_case_123"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := ToSnakeCase(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("ToSnakeCase(%q) = %q; expected %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpperSnakeCase(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"snake_case", "SNAKE_CASE"},
|
||||
{"CamelCase", "CAMEL_CASE"},
|
||||
{"lowerCamelCase", "LOWER_CAMEL_CASE"},
|
||||
{"F", "F"},
|
||||
{"Foo", "FOO"},
|
||||
{"FooB", "FOO_B"},
|
||||
{"FooID", "FOO_ID"},
|
||||
{" FooBar\t", "FOO_BAR"},
|
||||
{"HTTPStatusCode", "HTTP_STATUS_CODE"},
|
||||
{"ParseURL.DoParse", "PARSE_URL_DO_PARSE"},
|
||||
{"Convert Space", "CONVERT_SPACE"},
|
||||
{"Convert-dash", "CONVERT_DASH"},
|
||||
{"Skip___MultipleUnderscores", "SKIP_MULTIPLE_UNDERSCORES"},
|
||||
{"Skip MultipleSpaces", "SKIP_MULTIPLE_SPACES"},
|
||||
{"Skip---MultipleDashes", "SKIP_MULTIPLE_DASHES"},
|
||||
{"Hello World", "HELLO_WORLD"},
|
||||
{"Multiple Words Example", "MULTIPLE_WORDS_EXAMPLE"},
|
||||
{"", ""},
|
||||
{"A", "A"},
|
||||
{"z", "Z"},
|
||||
{"Special-Characters_Test", "SPECIAL_CHARACTERS_TEST"},
|
||||
{"Numbers123Test", "NUMBERS_123_TEST"},
|
||||
{"Hello World!", "HELLO_WORLD"},
|
||||
{"Test@With#Symbols", "TEST_WITH_SYMBOLS"},
|
||||
{"ComplexCase123!@#", "COMPLEX_CASE_123"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := UpperSnakeCase(test.input)
|
||||
if result != test.expected {
|
||||
t.Errorf("UpperSnakeCase(%q) = %q; expected %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
144
stringcase/split.go
Normal file
144
stringcase/split.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type runeInfo struct {
|
||||
r rune
|
||||
}
|
||||
|
||||
// Checks whether or not the rune represented by rInfo is a digit.
|
||||
func (rInfo *runeInfo) isDigit() bool {
|
||||
return unicode.IsDigit(rInfo.r)
|
||||
}
|
||||
|
||||
// Checks whether or not the rune represented by rInfo is an uppercase rune.
|
||||
func (rInfo *runeInfo) isUppercase() bool {
|
||||
return unicode.IsUpper(rInfo.r)
|
||||
}
|
||||
|
||||
// A reader designed for reading "CamelCase" strings.
|
||||
type rdr struct {
|
||||
input string // The data this reader operates on.
|
||||
pos int // The position of this reader.
|
||||
hasNextRune bool // A flag indicating if there's a next rune.
|
||||
rdRune runeInfo // Information about the last rune that was read.
|
||||
nxtRune runeInfo // Information about the next rune that's about to be read.
|
||||
}
|
||||
|
||||
// Read the next rune from r.
|
||||
func (r *rdr) readRune() {
|
||||
r.rdRune = runeInfo{rune(r.input[r.pos])}
|
||||
r.pos = r.pos + 1
|
||||
r.hasNextRune = r.pos < len(r.input)
|
||||
|
||||
if r.hasNextRune {
|
||||
r.nxtRune = runeInfo{rune(r.input[r.pos])}
|
||||
}
|
||||
}
|
||||
|
||||
// Undo the last rune from r.
|
||||
func (r *rdr) unreadRune() {
|
||||
r.pos = r.pos - 1
|
||||
r.nxtRune = r.rdRune
|
||||
r.rdRune = runeInfo{rune(r.input[r.pos])}
|
||||
r.hasNextRune = true // NOTE: An undo operation means that there will be always a next rune.
|
||||
}
|
||||
|
||||
// Verify if the word that's currently read by r is a word that should NOT be split.
|
||||
// If noSplit contains a word that starts with the word that's currently read by r, this function returns true, false
|
||||
// otherwise.
|
||||
func (r *rdr) isNoSplitWord(sIdx int, noSplit []string) bool {
|
||||
return ContainsFn(noSplit, r.input[sIdx:r.pos+1], func(got, want string) bool {
|
||||
return strings.HasPrefix(got, want)
|
||||
})
|
||||
}
|
||||
|
||||
// Read the next part from r.
|
||||
// Each word in noSplit (if provided) is treated as a word that shouldn't be split.
|
||||
func (r *rdr) readNextPart(noSplit []string) string {
|
||||
sIdx := r.pos
|
||||
|
||||
r.readRune()
|
||||
|
||||
if r.rdRune.isDigit() {
|
||||
return r.readNumber(sIdx, noSplit)
|
||||
}
|
||||
|
||||
return r.readWord(sIdx, noSplit)
|
||||
}
|
||||
|
||||
// Read and return a number from r.
|
||||
func (r *rdr) readNumber(sIdx int, noSplit []string) string {
|
||||
if r.hasNextRune && r.nxtRune.isDigit() {
|
||||
for r.hasNextRune && (r.nxtRune.isDigit() || r.isNoSplitWord(sIdx, noSplit)) {
|
||||
r.readRune()
|
||||
}
|
||||
|
||||
return r.input[sIdx:r.pos]
|
||||
}
|
||||
|
||||
return r.input[sIdx:r.pos]
|
||||
}
|
||||
|
||||
// Read and return a word from r.
|
||||
func (r *rdr) readWord(sIdx int, noSplit []string) string {
|
||||
if r.hasNextRune && r.nxtRune.isUppercase() {
|
||||
for r.hasNextRune && (r.nxtRune.isUppercase() || r.isNoSplitWord(sIdx, noSplit)) {
|
||||
r.readRune()
|
||||
}
|
||||
|
||||
if r.hasNextRune && (!r.nxtRune.isUppercase() && !r.nxtRune.isDigit()) {
|
||||
r.unreadRune()
|
||||
}
|
||||
|
||||
return r.input[sIdx:r.pos]
|
||||
}
|
||||
|
||||
for r.hasNextRune && (r.isNoSplitWord(sIdx, noSplit) || (!r.nxtRune.isUppercase() && !r.nxtRune.isDigit())) {
|
||||
r.readRune()
|
||||
}
|
||||
|
||||
return r.input[sIdx:r.pos]
|
||||
}
|
||||
|
||||
// Split reads v treating it as a "CamelCase" and returns the different words.
|
||||
// If v isn't a valid UTF-8 string, or when v is an empty string, a slice with one element (v) is returned.
|
||||
// Each word in noSplit (if provided) is treated as a word that shouldn't be split.
|
||||
func Split(input string, noSplit ...string) []string {
|
||||
if !utf8.ValidString(input) || len(input) == 0 {
|
||||
return []string{input}
|
||||
}
|
||||
|
||||
output := make([]string, 0)
|
||||
|
||||
inputs := SplitByNonAlphanumeric(input)
|
||||
for _, v := range inputs {
|
||||
v = strings.TrimSpace(v)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
output = append(output, split(v, noSplit...)...)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func split(input string, noSplit ...string) []string {
|
||||
if !utf8.ValidString(input) || len(input) == 0 {
|
||||
return []string{input}
|
||||
}
|
||||
|
||||
vRdr := &rdr{input: input}
|
||||
output := make([]string, 0)
|
||||
|
||||
for vRdr.pos < len(input) {
|
||||
part := vRdr.readNextPart(noSplit)
|
||||
output = append(output, part)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
82
stringcase/split_test.go
Normal file
82
stringcase/split_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitSingle(t *testing.T) {
|
||||
input := "URL.DoParse"
|
||||
result := Split(input)
|
||||
t.Logf("Split(%q) = %q;", input, result)
|
||||
t.Log(input[:7])
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{"hello world", []string{"hello", "world"}},
|
||||
{"hello_world", []string{"hello", "world"}},
|
||||
{"hello-world", []string{"hello", "world"}},
|
||||
{"hello.world", []string{"hello", "world"}},
|
||||
{"helloWorld", []string{"hello", "World"}},
|
||||
{"HelloWorld", []string{"Hello", "World"}},
|
||||
{"HTTPStatusCode", []string{"HTTP", "Status", "Code"}},
|
||||
{"ParseURLDoParse", []string{"Parse", "URL", "Do", "Parse"}},
|
||||
{"ParseUrlDoParse", []string{"Parse", "Url", "Do", "Parse"}},
|
||||
{"ParseUrl.DoParse", []string{"Parse", "Url", "Do", "Parse"}},
|
||||
{"ParseURL.DoParse", []string{"Parse", "URL", "Do", "Parse"}},
|
||||
{"ParseURL", []string{"Parse", "URL"}},
|
||||
{"ParseURL.", []string{"Parse", "URL"}},
|
||||
{"parse_url.do_parse", []string{"parse", "url", "do", "parse"}},
|
||||
{"convert space", []string{"convert", "space"}},
|
||||
{"convert-dash", []string{"convert", "dash"}},
|
||||
{"skip___multiple_underscores", []string{"skip", "multiple", "underscores"}},
|
||||
{"skip multiple spaces", []string{"skip", "multiple", "spaces"}},
|
||||
{"skip---multiple-dashes", []string{"skip", "multiple", "dashes"}},
|
||||
{"", []string{""}},
|
||||
{"a", []string{"a"}},
|
||||
{"Z", []string{"Z"}},
|
||||
{"special-characters_test", []string{"special", "characters", "test"}},
|
||||
{"numbers123test", []string{"numbers", "123", "test"}},
|
||||
{"hello world!", []string{"hello", "world"}},
|
||||
{"test@with#symbols", []string{"test", "with", "symbols"}},
|
||||
{"complexCase123!@#", []string{"complex", "Case", "123"}},
|
||||
|
||||
{"snake_case_string", []string{"snake", "case", "string"}},
|
||||
{"kebab-case-string", []string{"kebab", "case", "string"}},
|
||||
{"PascalCaseString", []string{"Pascal", "Case", "String"}},
|
||||
{"camelCaseString", []string{"camel", "Case", "String"}},
|
||||
{"HTTPRequest", []string{"HTTP", "Request"}},
|
||||
{"user ID", []string{"user", "ID"}},
|
||||
{"UserId", []string{"User", "Id"}},
|
||||
{"userID", []string{"user", "ID"}},
|
||||
{"UserID", []string{"User", "ID"}},
|
||||
{"123NumberPrefix", []string{"123", "Number", "Prefix"}},
|
||||
{"__leading_underscores", []string{"leading", "underscores"}},
|
||||
{"trailing_underscores__", []string{"trailing", "underscores"}},
|
||||
{"multiple___underscores", []string{"multiple", "underscores"}},
|
||||
{" spaces around ", []string{"spaces", "around"}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := Split(test.input)
|
||||
|
||||
if !compareStringSlices(result, test.expected) {
|
||||
t.Errorf("Split(%q) = %q; expected %q", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func compareStringSlices(slice1, slice2 []string) bool {
|
||||
if len(slice1) != len(slice2) {
|
||||
return false
|
||||
}
|
||||
for i := range slice1 {
|
||||
if slice1[i] != slice2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// ToSnakeCase 把字符转换为 蛇形命名法(snake_case)
|
||||
func ToSnakeCase(input string) string {
|
||||
if input == "" {
|
||||
return input
|
||||
}
|
||||
if len(input) == 1 {
|
||||
return strings.ToLower(input)
|
||||
}
|
||||
|
||||
input = strings.Replace(input, " ", "", -1)
|
||||
|
||||
source := []rune(input)
|
||||
dist := strings.Builder{}
|
||||
dist.Grow(len(input) + len(input)/3) // avoid reallocation memory, 33% ~ 50% is recommended
|
||||
skipNext := false
|
||||
for i := 0; i < len(source); i++ {
|
||||
cur := source[i]
|
||||
switch cur {
|
||||
case '-', '_':
|
||||
dist.WriteRune('_')
|
||||
skipNext = true
|
||||
continue
|
||||
}
|
||||
if unicode.IsLower(cur) || unicode.IsDigit(cur) {
|
||||
dist.WriteRune(cur)
|
||||
continue
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
dist.WriteRune(unicode.ToLower(cur))
|
||||
continue
|
||||
}
|
||||
|
||||
last := source[i-1]
|
||||
if (!unicode.IsLetter(last)) || unicode.IsLower(last) {
|
||||
if skipNext {
|
||||
skipNext = false
|
||||
} else {
|
||||
dist.WriteRune('_')
|
||||
}
|
||||
dist.WriteRune(unicode.ToLower(cur))
|
||||
continue
|
||||
}
|
||||
// last is upper case
|
||||
if i < len(source)-1 {
|
||||
next := source[i+1]
|
||||
if unicode.IsLower(next) {
|
||||
if skipNext {
|
||||
skipNext = false
|
||||
} else {
|
||||
dist.WriteRune('_')
|
||||
}
|
||||
dist.WriteRune(unicode.ToLower(cur))
|
||||
continue
|
||||
}
|
||||
}
|
||||
dist.WriteRune(unicode.ToLower(cur))
|
||||
}
|
||||
|
||||
return dist.String()
|
||||
}
|
||||
|
||||
// ToPascalCase 把字符转换为 帕斯卡命名/大驼峰命名法(CamelCase)
|
||||
func ToPascalCase(input string) string {
|
||||
return toCamelCase(input, true)
|
||||
}
|
||||
|
||||
// ToLowCamelCase 把字符转换为 小驼峰命名法(lowerCamelCase)
|
||||
func ToLowCamelCase(input string) string {
|
||||
return toCamelCase(input, false)
|
||||
}
|
||||
|
||||
func toCamelCase(s string, initCase bool) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
var uppercaseAcronym = map[string]string{}
|
||||
if a, ok := uppercaseAcronym[s]; ok {
|
||||
s = a
|
||||
}
|
||||
|
||||
n := strings.Builder{}
|
||||
n.Grow(len(s))
|
||||
capNext := initCase
|
||||
for i, v := range []byte(s) {
|
||||
vIsCap := v >= 'A' && v <= 'Z'
|
||||
vIsLow := v >= 'a' && v <= 'z'
|
||||
if capNext {
|
||||
if vIsLow {
|
||||
v += 'A'
|
||||
v -= 'a'
|
||||
}
|
||||
} else if i == 0 {
|
||||
if vIsCap {
|
||||
v += 'a'
|
||||
v -= 'A'
|
||||
}
|
||||
}
|
||||
if vIsCap || vIsLow {
|
||||
n.WriteByte(v)
|
||||
capNext = false
|
||||
} else if vIsNum := v >= '0' && v <= '9'; vIsNum {
|
||||
n.WriteByte(v)
|
||||
capNext = true
|
||||
} else {
|
||||
capNext = v == '_' || v == ' ' || v == '-' || v == '.'
|
||||
}
|
||||
}
|
||||
return n.String()
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToLowCamelCase(t *testing.T) {
|
||||
fmt.Println(ToLowCamelCase("snake_case"))
|
||||
fmt.Println(ToLowCamelCase("CamelCase"))
|
||||
fmt.Println(ToLowCamelCase("lowerCamelCase"))
|
||||
}
|
||||
|
||||
func TestToPascalCase(t *testing.T) {
|
||||
fmt.Println(ToPascalCase("snake_case"))
|
||||
fmt.Println(ToPascalCase("CamelCase"))
|
||||
fmt.Println(ToPascalCase("lowerCamelCase"))
|
||||
}
|
||||
|
||||
func TestToSnakeCase(t *testing.T) {
|
||||
fmt.Println(ToSnakeCase("snake_case"))
|
||||
fmt.Println(ToSnakeCase("CamelCase"))
|
||||
fmt.Println(ToSnakeCase("lowerCamelCase"))
|
||||
}
|
||||
132
stringcase/utils.go
Normal file
132
stringcase/utils.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func isLower(ch rune) bool {
|
||||
return ch >= 'a' && ch <= 'z'
|
||||
}
|
||||
|
||||
func toLower(ch rune) rune {
|
||||
if ch >= 'A' && ch <= 'Z' {
|
||||
return ch + 32
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func isUpper(ch rune) bool {
|
||||
return ch >= 'A' && ch <= 'Z'
|
||||
}
|
||||
|
||||
func toUpper(ch rune) rune {
|
||||
if ch >= 'a' && ch <= 'z' {
|
||||
return ch - 32
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func isSpace(ch rune) bool {
|
||||
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
|
||||
}
|
||||
|
||||
func isDigit(ch rune) bool {
|
||||
return ch >= '0' && ch <= '9'
|
||||
}
|
||||
|
||||
func isDelimiter(ch rune) bool {
|
||||
return ch == '-' || ch == '_' || isSpace(ch)
|
||||
}
|
||||
|
||||
type iterFunc func(prev, curr, next rune)
|
||||
|
||||
func stringIter(s string, callback iterFunc) {
|
||||
var prev rune
|
||||
var curr rune
|
||||
for _, next := range s {
|
||||
if curr == 0 {
|
||||
prev = curr
|
||||
curr = next
|
||||
continue
|
||||
}
|
||||
|
||||
callback(prev, curr, next)
|
||||
|
||||
prev = curr
|
||||
curr = next
|
||||
}
|
||||
|
||||
if len(s) > 0 {
|
||||
callback(prev, curr, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func isAlpha(r rune) bool {
|
||||
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
|
||||
}
|
||||
|
||||
func ReplaceNonAlphanumeric(s string, replacement string) string {
|
||||
if replacement == "" {
|
||||
replacement = "_"
|
||||
}
|
||||
// 使用正则表达式匹配非英文字母和数字的字符
|
||||
re := regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
// 替换为指定字符
|
||||
return re.ReplaceAllString(s, replacement)
|
||||
}
|
||||
|
||||
func SplitByNonAlphanumeric(input string) []string {
|
||||
var builder strings.Builder
|
||||
for _, r := range input {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
builder.WriteRune(r)
|
||||
} else {
|
||||
builder.WriteRune(' ') // 将非英文字符和数字的字符替换为空格
|
||||
}
|
||||
}
|
||||
processedInput := builder.String()
|
||||
return strings.Fields(processedInput) // 使用空格分割字符串
|
||||
}
|
||||
|
||||
func SplitAndKeepDelimiters(input string) []string {
|
||||
var result []string
|
||||
var builder strings.Builder
|
||||
|
||||
for _, r := range input {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
builder.WriteRune(r)
|
||||
} else {
|
||||
if builder.Len() > 0 {
|
||||
result = append(result, builder.String())
|
||||
builder.Reset()
|
||||
}
|
||||
result = append(result, string(r)) // 保留分隔符
|
||||
}
|
||||
}
|
||||
|
||||
if builder.Len() > 0 {
|
||||
result = append(result, builder.String())
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func ContainsFn[T any](slice []T, value T, predicate func(got, want T) bool) bool {
|
||||
for _, item := range slice {
|
||||
if predicate(item, value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isUpperCaseWord(word string) bool {
|
||||
for _, r := range word {
|
||||
if !unicode.IsUpper(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
52
stringcase/utils_test.go
Normal file
52
stringcase/utils_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package stringcase
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitByNonAlphanumeric(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{"hello-world", []string{"hello", "world"}},
|
||||
{"hello_world", []string{"hello", "world"}},
|
||||
{"hello.world", []string{"hello", "world"}},
|
||||
{"hello world", []string{"hello", "world"}},
|
||||
{"hello123world", []string{"hello123world"}},
|
||||
{"hello123 world", []string{"hello123", "world"}},
|
||||
{"hello-world_123", []string{"hello", "world", "123"}},
|
||||
{"!hello@world#", []string{"hello", "world"}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := SplitByNonAlphanumeric(test.input)
|
||||
if !reflect.DeepEqual(result, test.expected) {
|
||||
t.Errorf("SplitByNonAlphanumeric(%q) = %v; expected %v", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitAndKeepDelimiters(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected []string
|
||||
}{
|
||||
{"hello-world", []string{"hello", "-", "world"}},
|
||||
{"hello_world", []string{"hello", "_", "world"}},
|
||||
{"hello.world", []string{"hello", ".", "world"}},
|
||||
{"hello world", []string{"hello", " ", "world"}},
|
||||
{"hello123world", []string{"hello123world"}},
|
||||
{"hello123 world", []string{"hello123", " ", "world"}},
|
||||
{"hello-world_123", []string{"hello", "-", "world", "_", "123"}},
|
||||
{"!hello@world#", []string{"!", "hello", "@", "world", "#"}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := SplitAndKeepDelimiters(test.input)
|
||||
if !reflect.DeepEqual(result, test.expected) {
|
||||
t.Errorf("SplitAndKeepDelimiters(%q) = %v; expected %v", test.input, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
5
tag.bat
5
tag.bat
@@ -1,4 +1,4 @@
|
||||
git tag v1.1.28
|
||||
git tag v1.1.29
|
||||
|
||||
git tag bank_card/v1.1.5
|
||||
git tag geoip/v1.1.5
|
||||
@@ -10,8 +10,9 @@ git tag slug/v0.0.1
|
||||
git tag name_generator/v0.0.1
|
||||
git tag mapper/v0.0.3
|
||||
git tag password/v0.0.1
|
||||
git tag query_parser/v0.0.2
|
||||
|
||||
git tag entgo/v1.1.31
|
||||
git tag entgo/v1.1.32
|
||||
git tag gorm/v1.1.6
|
||||
|
||||
git push origin --tags
|
||||
|
||||
Reference in New Issue
Block a user