diff --git a/.CHANGELOG.md b/.CHANGELOG.md index 5436d92..2e16860 100644 --- a/.CHANGELOG.md +++ b/.CHANGELOG.md @@ -1,6 +1,8 @@ # 开发中 - [syncx: 支持分key加锁](https://github.com/ecodeclub/ekit/pull/224) - [syncx: 添加具有最大申请次数限制的LimitPool](https://github.com/ecodeclub/ekit/pull/233) +- [tuple: 增加Pair的实现](https://github.com/ecodeclub/ekit/pull/237) +- [randx: 重构randx.RandCode的代码,增加对特殊字符的支持](https://github.com/ecodeclub/ekit/pull/241) # v0.0.8 - [atomicx: 泛型封装 atomic.Value](https://github.com/gotomicro/ekit/pull/101) diff --git a/randx/rand_code.go b/randx/rand_code.go index 776d055..248a020 100644 --- a/randx/rand_code.go +++ b/randx/rand_code.go @@ -17,36 +17,99 @@ package randx import ( "errors" "math/rand" + + "github.com/ecodeclub/ekit/tuple/pair" ) -var ERRTYPENOTSUPPORTTED = errors.New("ekit:不支持的类型") +var ( + errTypeNotSupported = errors.New("ekit:不支持的类型") + errLengthLessThanZero = errors.New("ekit:长度必须大于等于0") +) type TYPE int const ( - TYPE_DEFAULT TYPE = 0 //默认类型 - TYPE_DIGIT TYPE = 1 //数字// - TYPE_LETTER TYPE = 2 //小写字母 - TYPE_CAPITAL TYPE = 3 //大写字母 - TYPE_MIXED TYPE = 4 //数字+字母混合 + // 数字 + TYPE_DIGIT TYPE = 1 + // 小写字母 + TYPE_LOWERCASE TYPE = 1 << 1 + TYPE_LETTER TYPE = TYPE_LOWERCASE + // 大写字母 + TYPE_UPPERCASE TYPE = 1 << 2 + TYPE_CAPITAL TYPE = TYPE_UPPERCASE + // 特殊符号 + TYPE_SPECIAL TYPE = 1 << 3 + // 混合类型 + TYPE_MIXED = (TYPE_DIGIT | TYPE_UPPERCASE | TYPE_LOWERCASE | TYPE_SPECIAL) + + // 数字字符组 + CHARSET_DIGIT = "0123456789" + // 小写字母字符组 + CHARSET_LOWERCASE = "abcdefghijklmnopqrstuvwxyz" + CHARSET_LETTER = CHARSET_LOWERCASE + // 大写字母字符组 + CHARSET_UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + CHARSET_CAPITAL = CHARSET_UPPERCASE + // 特殊字符数组 + CHARSET_SPECIAL = " ~!@#$%^&*()_+-=[]{};'\\:\"|,./<>?" +) + +var ( + // 只限于randx包内部使用 + typeCharsetPairs = []pair.Pair[TYPE, string]{ + pair.NewPair(TYPE_DIGIT, CHARSET_DIGIT), + pair.NewPair(TYPE_LOWERCASE, CHARSET_LOWERCASE), + pair.NewPair(TYPE_UPPERCASE, CHARSET_UPPERCASE), + pair.NewPair(TYPE_SPECIAL, CHARSET_SPECIAL), + } ) -// RandCode 根据传入的长度和类型生成随机字符串,这个方法目前可以生成数字、字母、数字+字母的随机字符串 +// RandCode 根据传入的长度和类型生成随机字符串 +// 请保证输入的 length >= 0,否则会返回 errLengthLessThanZero +// 请保证输入的 typ 的取值范围在 (0, type.MIXED] 内,否则会返回 errTypeNotSupported func RandCode(length int, typ TYPE) (string, error) { - switch typ { - case TYPE_DEFAULT: - fallthrough - case TYPE_DIGIT: - return generate("0123456789", length, 4), nil - case TYPE_LETTER: - return generate("abcdefghijklmnopqrstuvwxyz", length, 5), nil - case TYPE_CAPITAL: - return generate("ABCDEFGHIJKLMNOPQRSTUVWXYZ", length, 5), nil - case TYPE_MIXED: - return generate("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", length, 7), nil - default: - return "", ERRTYPENOTSUPPORTTED + if length < 0 { + return "", errLengthLessThanZero + } + if length == 0 { + return "", nil + } + if typ > TYPE_MIXED { + return "", errTypeNotSupported + } + charset := "" + for _, p := range typeCharsetPairs { + if (typ & p.Key) == p.Key { + charset += p.Value + } + } + return RandStrByCharset(length, charset) +} + +// 根据传入的长度和字符集生成随机字符串 +// 请保证输入的 length >= 0,否则会返回 errLengthLessThanZero +// 请保证输入的字符集不为空字符串,否则会返回 errTypeNotSupported +// 字符集内部字符可以无序或重复 +func RandStrByCharset(length int, charset string) (string, error) { + if length < 0 { + return "", errLengthLessThanZero + } + if length == 0 { + return "", nil + } + charsetSize := len(charset) + if charsetSize == 0 { + return "", errTypeNotSupported + } + return generate(charset, length, getFirstMask(charsetSize)), nil +} + +func getFirstMask(charsetSize int) int { + bits := 0 + for charsetSize > ((1 << bits) - 1) { + bits++ } + return bits } // generate 根据传入的随机源和长度生成随机字符串,一次随机,多次使用 diff --git a/randx/rand_code_test.go b/randx/rand_code_test.go index ee962ee..255162f 100644 --- a/randx/rand_code_test.go +++ b/randx/rand_code_test.go @@ -12,81 +12,233 @@ // See the License for the specific language governing permissions and // limitations under the License. -package randx +package randx_test import ( "errors" "regexp" + "strings" "testing" + + "github.com/ecodeclub/ekit/randx" + "github.com/stretchr/testify/assert" +) + +var ( + errTypeNotSupported = errors.New("ekit:不支持的类型") + errLengthLessThanZero = errors.New("ekit:长度必须大于等于0") ) func TestRandCode(t *testing.T) { testCases := []struct { name string length int - typ TYPE + typ randx.TYPE wantMatch string wantErr error }{ { - name: "默认类型", - length: 8, - typ: TYPE_DEFAULT, + name: "数字验证码", + length: 100, + typ: randx.TYPE_DIGIT, wantMatch: "^[0-9]+$", wantErr: nil, }, { - name: "数字验证码", - length: 8, - typ: TYPE_DIGIT, - wantMatch: "^[0-9]+$", - wantErr: nil, - }, { name: "小写字母验证码", - length: 8, - typ: TYPE_LETTER, + length: 100, + typ: randx.TYPE_LETTER, wantMatch: "^[a-z]+$", wantErr: nil, - }, { + }, + { + name: "数字+小写字母验证码", + length: 100, + typ: randx.TYPE_DIGIT | randx.TYPE_LOWERCASE, + wantMatch: "^[a-z0-9]+$", + wantErr: nil, + }, + { + name: "数字+大写字母验证码", + length: 100, + typ: randx.TYPE_DIGIT | randx.TYPE_UPPERCASE, + wantMatch: "^[A-Z0-9]+$", + wantErr: nil, + }, + { name: "大写字母验证码", - length: 8, - typ: TYPE_CAPITAL, + length: 100, + typ: randx.TYPE_CAPITAL, + wantMatch: "^[A-Z]+$", + wantErr: nil, + }, + { + name: "大写字母验证码(兼容旧版本)", + length: 100, + typ: randx.TYPE_CAPITAL, wantMatch: "^[A-Z]+$", wantErr: nil, - }, { - name: "混合验证码", - length: 8, - typ: TYPE_MIXED, + }, + { + name: "大小写字母验证码", + length: 100, + typ: randx.TYPE_UPPERCASE | randx.TYPE_LOWERCASE, + wantMatch: "^[a-zA-Z]+$", + wantErr: nil, + }, + { + name: "大小写字母验证码(兼容旧版本)", + length: 100, + typ: randx.TYPE_CAPITAL | randx.TYPE_LETTER, + wantMatch: "^[a-zA-Z]+$", + wantErr: nil, + }, + { + name: "数字+大小写字母验证码", + length: 100, + typ: randx.TYPE_DIGIT | randx.TYPE_UPPERCASE | randx.TYPE_LOWERCASE, wantMatch: "^[0-9a-zA-Z]+$", wantErr: nil, - }, { - name: "未定义类型", - length: 8, - typ: 9, + }, + { + name: "数字+大小写字母验证码(兼容旧版本)", + length: 100, + typ: randx.TYPE_DIGIT | randx.TYPE_LETTER | randx.TYPE_CAPITAL, + wantMatch: "^[0-9a-zA-Z]+$", + wantErr: nil, + }, + { + name: "所有类型验证", + length: 100, + typ: randx.TYPE_MIXED, + wantMatch: "^[\\S\\s]+$", + wantErr: nil, + }, + { + name: "特殊字符类型验证", + length: 100, + typ: randx.TYPE_SPECIAL, + wantMatch: "^[^0-9a-zA-Z]+$", + wantErr: nil, + }, + { + name: "未定义类型(超过范围)", + length: 100, + typ: randx.TYPE_MIXED + 1, + wantMatch: "", + wantErr: errTypeNotSupported, + }, + { + name: "未定义类型(0)", + length: 100, + typ: 0, + wantMatch: "", + wantErr: errTypeNotSupported, + }, + { + name: "长度小于0", + length: -1, + typ: 0, + wantMatch: "", + wantErr: errLengthLessThanZero, + }, + { + name: "长度等于0", + length: 0, + typ: randx.TYPE_MIXED, wantMatch: "", - wantErr: ERRTYPENOTSUPPORTTED, + wantErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + code, err := randx.RandCode(tc.length, tc.typ) + if tc.wantErr != nil { + assert.Equal(t, tc.wantErr, err) + return + } + assert.Len(t, code, tc.length) + if tc.length > 0 { + matched, err := regexp.MatchString(tc.wantMatch, code) + assert.Nil(t, err) + assert.Truef(t, matched, "expected %s but got %s", tc.wantMatch, code) + } + + }) + } +} + +func TestRandStrByCharset(t *testing.T) { + matchFunc := func(str, charset string) bool { + for _, c := range str { + if !strings.Contains(charset, string(c)) { + return false + } + } + return true + } + testCases := []struct { + name string + length int + charset string + wantErr error + }{ + { + name: "长度小于0", + length: -1, + charset: "123", + wantErr: errLengthLessThanZero, + }, + { + name: "长度等于0", + length: 0, + charset: "123", + wantErr: nil, + }, + { + name: "随机字符串测试", + length: 100, + charset: "2rg248ry227t@@", + wantErr: nil, + }, + { + name: "随机字符串测试", + length: 100, + charset: "2rg248ry227t@&*($.!", + wantErr: nil, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - code, err := RandCode(tc.length, tc.typ) - if err != nil { - if !errors.Is(err, tc.wantErr) { - t.Errorf("unexpected error: %v", err) - } - } else { - //长度检验 - if len(code) != tc.length { - t.Errorf("expected length: %d but got length:%d ", tc.length, len(code)) - } - //模式检验 - matched, _ := regexp.MatchString(tc.wantMatch, code) - if !matched { - t.Errorf("expected %s but got %s", tc.wantMatch, code) - } + code, err := randx.RandStrByCharset(tc.length, tc.charset) + if tc.wantErr != nil { + assert.Equal(t, tc.wantErr, err) + return } + + assert.Len(t, code, tc.length) + if tc.length > 0 { + assert.True(t, matchFunc(code, tc.charset)) + } + }) } +} +// goos: linux +// goarch: amd64 +// pkg: github.com/ecodeclub/ekit/randx +// cpu: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz +// BenchmarkRandCode_MIXED/length=1000000-8 1000000000 0.004584 ns/op 0 B/op 0 allocs/op +func BenchmarkRandCode_MIXED(b *testing.B) { + b.Run("length=1000000", func(b *testing.B) { + n := 1000000 + b.StartTimer() + res, err := randx.RandCode(n, randx.TYPE_MIXED) + b.StopTimer() + assert.Nil(b, err) + assert.Len(b, res, n) + }) }