mirror of
https://github.com/jimeh/rands.git
synced 2026-02-19 03:16:39 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c98b79b4c5 | ||
|
b28e506295
|
|||
| a141938394 | |||
|
|
312e856234 | ||
|
f4dccc6e46
|
|||
|
ec94efe49e
|
|||
| fe4308607c | |||
| e87d9c4726 | |||
| 127ebbaa03 | |||
|
|
41227bd53d | ||
| 16bd3ea3b9 | |||
| 8da5e1ef80 | |||
|
1b0bb32a3e
|
|||
|
660fc4d179
|
|||
| 4642a149bb | |||
|
22fe517baa
|
|||
|
74dd8fb7e9
|
|||
|
a86282e34d
|
|||
|
0a42d1e112
|
|||
|
b59d421322
|
|||
|
164ccc497a
|
|||
|
83bad54113
|
|||
|
09883ed0d2
|
|||
| d7f439e1d9 | |||
|
825a3c18fb
|
|||
|
a755fe957a
|
|||
|
ef9cb3a01d
|
3
.github/.release-please-manifest.json
vendored
Normal file
3
.github/.release-please-manifest.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "0.5.0"
|
||||
}
|
||||
15
.github/release-please-config.json
vendored
Normal file
15
.github/release-please-config.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"bootstrap-sha": "660fc4d179bfe22826230988407fff810e3b8568",
|
||||
"packages": {
|
||||
".": {
|
||||
"release-type": "go",
|
||||
"changelog-path": "CHANGELOG.md",
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": true,
|
||||
"always-update": true,
|
||||
"draft": false,
|
||||
"prerelease": false
|
||||
}
|
||||
},
|
||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
||||
}
|
||||
107
.github/workflows/ci.yml
vendored
107
.github/workflows/ci.yml
vendored
@@ -7,11 +7,14 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
version: v1.35
|
||||
go-version-file: "go.mod"
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.64
|
||||
env:
|
||||
VERBOSE: "true"
|
||||
|
||||
@@ -19,34 +22,22 @@ jobs:
|
||||
name: Tidy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version-file: "go.mod"
|
||||
- name: Check if mods are tidy
|
||||
run: make check-tidy
|
||||
|
||||
benchmark:
|
||||
name: Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref != 'refs/heads/master'
|
||||
if: github.ref != 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version-file: "go.mod"
|
||||
- name: Run benchmarks
|
||||
run: make bench | tee output.raw
|
||||
- name: Fix benchmark names
|
||||
@@ -67,18 +58,12 @@ jobs:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version-file: "go.mod"
|
||||
- name: Publish coverage
|
||||
uses: paambaati/codeclimate-action@v2.7.4
|
||||
uses: paambaati/codeclimate-action@v9
|
||||
env:
|
||||
VERBOSE: "true"
|
||||
GOMAXPROCS: 4
|
||||
@@ -92,17 +77,23 @@ jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.18"
|
||||
- "1.19"
|
||||
- "1.20"
|
||||
- "1.21"
|
||||
- "1.22"
|
||||
- "1.23"
|
||||
- "1.24"
|
||||
- "stable"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version: ${{ matrix.go-version }}
|
||||
check-latest: true
|
||||
- name: Run tests
|
||||
run: make test
|
||||
env:
|
||||
@@ -111,18 +102,15 @@ jobs:
|
||||
benchmark-store:
|
||||
name: Store benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
if: github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
deployments: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version-file: "go.mod"
|
||||
- name: Run benchmarks
|
||||
run: make bench | tee output.raw
|
||||
- name: Fix benchmark names
|
||||
@@ -134,6 +122,19 @@ jobs:
|
||||
with:
|
||||
tool: "go"
|
||||
output-file-path: output.txt
|
||||
github-token: ${{ secrets.GH_PUSH_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-on-alert: true
|
||||
auto-push: true
|
||||
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
outputs:
|
||||
release_created: ${{ steps.release-please.outputs.release_created }}
|
||||
version: ${{ steps.release-please.outputs.version }}
|
||||
steps:
|
||||
- uses: jimeh/release-please-manifest-action@v2
|
||||
id: release-please
|
||||
with:
|
||||
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -4,59 +4,61 @@ linters-settings:
|
||||
statements: 150
|
||||
gocyclo:
|
||||
min-complexity: 20
|
||||
golint:
|
||||
min-confidence: 0
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
lll:
|
||||
line-length: 80
|
||||
tab-width: 4
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- depguard
|
||||
- dupl
|
||||
- copyloopvar
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- funlen
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- goerr113
|
||||
- godot
|
||||
- gofumpt
|
||||
- goimports
|
||||
- golint
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- importas
|
||||
- ineffassign
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- nilerr
|
||||
- nlreturn
|
||||
- noctx
|
||||
- nolintlint
|
||||
- scopelint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
issues:
|
||||
include:
|
||||
# - EXC0002 # disable excluding of issues about comments from golint
|
||||
exclude:
|
||||
- Using the variable on range scope `tt` in function literal
|
||||
- Using the variable on range scope `tc` in function literal
|
||||
@@ -71,6 +73,12 @@ issues:
|
||||
- source: "`json:"
|
||||
linters:
|
||||
- lll
|
||||
- source: "`xml:"
|
||||
linters:
|
||||
- lll
|
||||
- source: "`yaml:"
|
||||
linters:
|
||||
- lll
|
||||
|
||||
run:
|
||||
timeout: 2m
|
||||
|
||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -2,6 +2,49 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [0.5.0](https://github.com/jimeh/rands/compare/v0.4.0...v0.5.0) (2025-03-10)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **shuffle:** Minimum required Go version is now 1.18 due the `ShuffleSlice` using generics.
|
||||
|
||||
### Features
|
||||
|
||||
* **shuffle:** add Shuffle and ShuffleSlice functions ([#11](https://github.com/jimeh/rands/issues/11)) ([a141938](https://github.com/jimeh/rands/commit/a14193839475562268ea10e356580b33b27ebedc))
|
||||
|
||||
## [0.4.0](https://github.com/jimeh/rands/compare/v0.3.0...v0.4.0) (2025-03-07)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **deps:** Minimum Go version changed from 1.15 to 1.17.
|
||||
|
||||
### Features
|
||||
|
||||
* **deps:** upgrade to Go 1.17 and golanci-lint 1.64 ([#5](https://github.com/jimeh/rands/issues/5)) ([16bd3ea](https://github.com/jimeh/rands/commit/16bd3ea3b9483f5510c0e0fa35e832e881840b3a))
|
||||
* **strings/uuidv7:** add UUIDv7 generation ([#10](https://github.com/jimeh/rands/issues/10)) ([fe43086](https://github.com/jimeh/rands/commit/fe4308607cc8d454255908dc44e64462759e303d))
|
||||
|
||||
## [0.3.0](https://github.com/jimeh/rands/compare/v0.2.0...v0.3.0) (2021-12-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **error:** export error variables ([164ccc4](https://github.com/jimeh/rands/commit/164ccc497ad9880d43b22b74d6a83c5e68d79334))
|
||||
* **randsmust:** add randsmust package ([22fe517](https://github.com/jimeh/rands/commit/22fe517baa8b6939503c0c804dd71628f7d473a3))
|
||||
|
||||
## [0.2.0](https://github.com/jimeh/rands/compare/v0.1.1...v0.2.0) (2021-03-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **strings:** add UUID function to generate random RFC 4122 UUID v4 strings ([825a3c1](https://github.com/jimeh/rands/commit/825a3c18fbc8e0497eafea9254baadb2951f23c6))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **strings:** add missing benchmark for DNSLabel ([a755fe9](https://github.com/jimeh/rands/commit/a755fe957a485e4c29b8ade474878b265785bc66))
|
||||
|
||||
### [0.1.1](https://github.com/jimeh/rands/compare/v0.1.0...v0.1.1) (2021-01-20)
|
||||
|
||||
## 0.1.0 (2021-01-20)
|
||||
|
||||
51
Makefile
51
Makefile
@@ -34,26 +34,19 @@ SHELL := env \
|
||||
# Tools
|
||||
#
|
||||
|
||||
TOOLS += $(TOOLDIR)/gobin
|
||||
gobin: $(TOOLDIR)/gobin
|
||||
$(TOOLDIR)/gobin:
|
||||
GO111MODULE=off go get -u github.com/myitcv/gobin
|
||||
|
||||
# external tool
|
||||
define tool # 1: binary-name, 2: go-import-path
|
||||
TOOLS += $(TOOLDIR)/$(1)
|
||||
|
||||
.PHONY: $(1)
|
||||
$(1): $(TOOLDIR)/$(1)
|
||||
|
||||
$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile
|
||||
gobin $(V) "$(2)"
|
||||
$(TOOLDIR)/$(1): Makefile
|
||||
GOBIN="$(CURDIR)/$(TOOLDIR)" go install "$(2)"
|
||||
endef
|
||||
|
||||
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc))
|
||||
$(eval $(call tool,gofumports,mvdan.cc/gofumpt/gofumports))
|
||||
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.35))
|
||||
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod))
|
||||
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc@latest))
|
||||
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
|
||||
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports@latest))
|
||||
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64))
|
||||
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest))
|
||||
|
||||
.PHONY: tools
|
||||
tools: $(TOOLS)
|
||||
@@ -62,8 +55,8 @@ tools: $(TOOLS)
|
||||
# Development
|
||||
#
|
||||
|
||||
TEST ?= $$(go list ./... | grep -v 'vendor')
|
||||
BENCH ?= .
|
||||
TESTARGS ?=
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@@ -72,24 +65,24 @@ clean:
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test $(V) -count=1 $(TESTARGS) $(TEST)
|
||||
go test $(V) -count=1 -race $(TESTARGS) ./...
|
||||
|
||||
.PHONY: test-deps
|
||||
test-deps:
|
||||
go test all
|
||||
|
||||
.PHONY: lint
|
||||
lint: golangci-lint
|
||||
GOGC=off golangci-lint $(V) run
|
||||
lint: $(TOOLDIR)/golangci-lint
|
||||
golangci-lint $(V) run
|
||||
|
||||
.PHONY: format
|
||||
format: gofumports
|
||||
gofumports -w .
|
||||
format: $(TOOLDIR)/goimports $(TOOLDIR)/gofumpt
|
||||
goimports -w . && gofumpt -w .
|
||||
|
||||
.SILENT: bench
|
||||
.PHONY: bench
|
||||
bench:
|
||||
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) $(TEST)
|
||||
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./...
|
||||
|
||||
#
|
||||
# Coverage
|
||||
@@ -115,16 +108,14 @@ coverage.out: $(SOURCES)
|
||||
|
||||
.PHONY: deps
|
||||
deps:
|
||||
$(info Downloading dependencies)
|
||||
go mod download
|
||||
|
||||
.PHONY: deps-update
|
||||
deps-update:
|
||||
$(info Downloading dependencies)
|
||||
go get -u ./...
|
||||
go get -u -t ./...
|
||||
|
||||
.PHONY: deps-analyze
|
||||
deps-analyze: gomod
|
||||
deps-analyze: $(TOOLDIR)/gomod
|
||||
gomod analyze
|
||||
|
||||
.PHONY: tidy
|
||||
@@ -160,14 +151,6 @@ check-tidy:
|
||||
|
||||
# Serve docs
|
||||
.PHONY: docs
|
||||
docs: godoc
|
||||
docs: $(TOOLDIR)/godoc
|
||||
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
|
||||
@godoc -http=127.0.0.1:6060
|
||||
|
||||
#
|
||||
# Release
|
||||
#
|
||||
|
||||
.PHONY: new-version
|
||||
new-version:
|
||||
npx standard-version
|
||||
|
||||
146
README.md
146
README.md
@@ -5,71 +5,117 @@
|
||||
<p align="center">
|
||||
<strong>
|
||||
Go package providing a suite of functions that use <code>crypto/rand</code>
|
||||
to generate cryptographically secure random strings in various formats, as
|
||||
well as ints and bytes.
|
||||
to generate cryptographically secure random data in various forms and
|
||||
formats.
|
||||
</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://pkg.go.dev/github.com/jimeh/rands">
|
||||
<img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white"
|
||||
alt="Go Reference">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/releases">
|
||||
<img src="https://img.shields.io/github/v/tag/jimeh/rands?label=release" alt="GitHub tag (latest SemVer)">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/actions">
|
||||
<img src="https://img.shields.io/github/workflow/status/jimeh/rands/CI.svg?logo=github" alt="Actions Status">
|
||||
</a>
|
||||
<a href="https://codeclimate.com/github/jimeh/rands">
|
||||
<img src="https://img.shields.io/codeclimate/coverage/jimeh/rands.svg?logo=code%20climate" alt="Coverage">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/issues">
|
||||
<img src="https://img.shields.io/github/issues-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white"
|
||||
alt="GitHub issues">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/pulls">
|
||||
<img src="https://img.shields.io/github/issues-pr-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/jimeh/rands.svg?style=flat" alt="License Status">
|
||||
</a>
|
||||
<a href="https://pkg.go.dev/github.com/jimeh/rands"><img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white" alt="Go Reference"></a>
|
||||
<a href="https://github.com/jimeh/rands/releases"><img src="https://img.shields.io/github/v/tag/jimeh/rands?label=release" alt="GitHub tag (latest SemVer)"></a>
|
||||
<a href="https://github.com/jimeh/rands/actions"><img src="https://img.shields.io/github/actions/workflow/status/jimeh/rands/ci.yml?branch=main&logo=github" alt="Actions Status"></a>
|
||||
<a href="https://codeclimate.com/github/jimeh/rands"><img src="https://img.shields.io/codeclimate/coverage/jimeh/rands.svg?logo=code%20climate" alt="Coverage"></a>
|
||||
<a href="https://github.com/jimeh/rands/issues"><img src="https://img.shields.io/github/issues-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub issues"></a>
|
||||
<a href="https://github.com/jimeh/rands/pulls"><img src="https://img.shields.io/github/issues-pr-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests"></a>
|
||||
<a href="https://github.com/jimeh/rands/blob/master/LICENSE"><img src="https://img.shields.io/github/license/jimeh/rands.svg?style=flat" alt="License Status"></a>
|
||||
</p>
|
||||
|
||||
```go
|
||||
s, _ := rands.Base64(16) // => CYxqEdUB1Rzno3SyZu2g/g==
|
||||
s, _ := rands.Base64URL(16) // => zlqw9aFqcFggbk2asn3_aQ
|
||||
s, _ := rands.Hex(16) // => 956e2ec9e7f19ddd58bb935826926531
|
||||
s, _ := rands.Alphanumeric(16) // => Fvk1PkrmG5crgOjT
|
||||
s, _ := rands.Alphabetic(16) // => XEJIzcZufHkuUmRM
|
||||
s, _ := rands.Upper(16) // => UMAGAFPPNDRGLUPZ
|
||||
s, _ := rands.UpperNumeric(16) // => DF0CQS0TK9CPUO3E
|
||||
s, _ := rands.Lower(16) // => ocsmggykzrxzfwgt
|
||||
s, _ := rands.LowerNumeric(16) // => rwlv7a1p7klqffs5
|
||||
s, _ := rands.Numeric(16) // => 9403373143598295
|
||||
## [`rands`](https://pkg.go.dev/github.com/jimeh/rands) package
|
||||
|
||||
s, _ := rands.String(16, "abcdefABCDEF") // => adCDCaDEdeffeDeb
|
||||
s, _ := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 下下口九力下土夕下土八上二夕大三
|
||||
`rands` is intended for use in production code where random data generation is
|
||||
required. All functions have a error return value which should be checked.
|
||||
|
||||
s, _ := rands.DNSLabel(16) // => z0ij9o8qkbs0ru-h
|
||||
For tests there is the `randsmust` package, which has all the same functions but
|
||||
with single return values, and they panic in the event of an error.
|
||||
|
||||
n, _ := rands.Int(2147483647) // => 1334400235
|
||||
n, _ := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
|
||||
|
||||
b, _ := rands.Bytes(8) // => [0 220 137 243 135 204 34 63]
|
||||
```
|
||||
|
||||
## Import
|
||||
### Import
|
||||
|
||||
```
|
||||
import "github.com/jimeh/rands"
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```go
|
||||
s, err := rands.Base64(16) // => CYxqEdUB1Rzno3SyZu2g/g==
|
||||
s, err := rands.Base64URL(16) // => zlqw9aFqcFggbk2asn3_aQ
|
||||
s, err := rands.Hex(16) // => 956e2ec9e7f19ddd58bb935826926531
|
||||
s, err := rands.Alphanumeric(16) // => Fvk1PkrmG5crgOjT
|
||||
s, err := rands.Alphabetic(16) // => XEJIzcZufHkuUmRM
|
||||
s, err := rands.Upper(16) // => UMAGAFPPNDRGLUPZ
|
||||
s, err := rands.UpperNumeric(16) // => DF0CQS0TK9CPUO3E
|
||||
s, err := rands.Lower(16) // => ocsmggykzrxzfwgt
|
||||
s, err := rands.LowerNumeric(16) // => rwlv7a1p7klqffs5
|
||||
s, err := rands.Numeric(16) // => 9403373143598295
|
||||
|
||||
s, err := rands.String(16, "abcdefABCDEF") // => adCDCaDEdeffeDeb
|
||||
s, err := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 下下口九力下土夕下土八上二夕大三
|
||||
|
||||
s, err := rands.DNSLabel(16) // => z0ij9o8qkbs0ru-h
|
||||
s, err := rands.UUID() // => a62b8712-f238-43ba-a47e-333f5fffe785
|
||||
s, err := rands.UUIDv7() // => 01954a31-867f-7ffb-876e-b818f960ec3b
|
||||
|
||||
n, err := rands.Int(2147483647) // => 1334400235
|
||||
n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
|
||||
|
||||
b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63]
|
||||
|
||||
err := rands.Shuffle(len(arr), func(i, j) { arr[i], arr[j] = arr[j], arr[i] })
|
||||
err := rands.ShuffleSlice(arr)
|
||||
```
|
||||
|
||||
## [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/randsmust) package
|
||||
|
||||
`randsmust` is specifically intended as an alternative to `rands` for use in
|
||||
tests. All functions return a single value, and panic in the event of an error.
|
||||
This makes them easy to use when building structs in test cases that need random
|
||||
data.
|
||||
|
||||
For production code, make sure to use the `rands` package and check returned
|
||||
errors.
|
||||
|
||||
### Import
|
||||
|
||||
```
|
||||
import "github.com/jimeh/rands/randsmust"
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```go
|
||||
s := randsmust.Base64(16) // => d1wm/wS6AQGduO3uaey1Cg==
|
||||
s := randsmust.Base64URL(16) // => 4pHWVcddXsL_45vhOfCdng
|
||||
s := randsmust.Hex(16) // => b5552558bc009264d129c422a666fe56
|
||||
s := randsmust.Alphanumeric(16) // => j5WkpNKmW8K701XF
|
||||
s := randsmust.Alphabetic(16) // => OXxsqfFjNLvmZqDb
|
||||
s := randsmust.Upper(16) // => AOTLYQRCVNMEPRCX
|
||||
s := randsmust.UpperNumeric(16) // => 1NTY6KATDVAXBTY2
|
||||
s := randsmust.Lower(16) // => xmftrwvurrritqfu
|
||||
s := randsmust.LowerNumeric(16) // => yszg56fzeql7pjpl
|
||||
s := randsmust.Numeric(16) // => 0761782105447226
|
||||
|
||||
s := randsmust.String(16, "abcdefABCDEF") // => dfAbBfaDDdDFDaEa
|
||||
s := randsmust.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 十十千口三十十下九上千口七夕土口
|
||||
|
||||
s := randsmust.DNSLabel(16) // => pu31o0gqyk76x35f
|
||||
s := randsmust.UUID() // => d616c873-f3dd-4690-bcd6-ed307eec1105
|
||||
s := randsmust.UUIDv7() // => 01954a30-add2-7590-8238-6cf6b2790c1e
|
||||
|
||||
n := randsmust.Int(2147483647) // => 1293388115
|
||||
n := randsmust.Int64(int64(9223372036854775807)) // => 6168113630900161239
|
||||
|
||||
b := randsmust.Bytes(8) // => [205 128 54 95 0 95 53 51]
|
||||
|
||||
randsmust.Shuffle(len(arr), func(i, j) { arr[i], arr[j] = arr[j], arr[i] })
|
||||
randsmust.ShuffleSlice(arr)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Please see the
|
||||
[Go Reference](https://pkg.go.dev/github.com/jimeh/rands#section-documentation)
|
||||
for documentation and examples.
|
||||
Please see the Go Reference for documentation and examples:
|
||||
|
||||
- [`rands`](https://pkg.go.dev/github.com/jimeh/rands)
|
||||
- [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/randsmust)
|
||||
|
||||
## Benchmarks
|
||||
|
||||
@@ -78,4 +124,4 @@ https://jimeh.me/rands/dev/bench/
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/jimeh/rands/blob/master/LICENSE)
|
||||
[MIT](https://github.com/jimeh/rands/blob/main/LICENSE)
|
||||
|
||||
@@ -2,11 +2,16 @@ package rands_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/jimeh/rands"
|
||||
)
|
||||
|
||||
func ExampleBytes() {
|
||||
b, _ := rands.Bytes(8)
|
||||
fmt.Printf("%+v\n", b) // => [0 220 137 243 135 204 34 63]
|
||||
b, err := rands.Bytes(8)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%+v\n", b) // => [181 153 143 235 241 20 208 173]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
)
|
||||
|
||||
func TestBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, _ := Bytes(tt.n)
|
||||
|
||||
10
go.mod
10
go.mod
@@ -1,5 +1,11 @@
|
||||
module github.com/jimeh/rands
|
||||
|
||||
go 1.15
|
||||
go 1.18
|
||||
|
||||
require github.com/stretchr/testify v1.7.0
|
||||
require github.com/stretchr/testify v1.10.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
13
go.sum
13
go.sum
@@ -1,11 +1,10 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
22
ints.go
22
ints.go
@@ -6,15 +6,15 @@ import (
|
||||
"math/big"
|
||||
)
|
||||
|
||||
var errInvalidMaxInt = fmt.Errorf("%w: max cannot be less than 1", errBase)
|
||||
var ErrInvalidMaxInt = fmt.Errorf("%w: max cannot be less than 1", Err)
|
||||
|
||||
// Int generates a random int ranging between 0 and max.
|
||||
func Int(max int) (int, error) {
|
||||
if max < 1 {
|
||||
return 0, errInvalidMaxInt
|
||||
// Int generates a random int ranging between 0 and nMax.
|
||||
func Int(nMax int) (int, error) {
|
||||
if nMax < 1 {
|
||||
return 0, ErrInvalidMaxInt
|
||||
}
|
||||
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(int64(nMax)))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -22,13 +22,13 @@ func Int(max int) (int, error) {
|
||||
return int(r.Int64()), nil
|
||||
}
|
||||
|
||||
// Int64 generates a random int64 ranging between 0 and max.
|
||||
func Int64(max int64) (int64, error) {
|
||||
if max < 1 {
|
||||
return 0, errInvalidMaxInt
|
||||
// Int64 generates a random int64 ranging between 0 and nMax.
|
||||
func Int64(nMax int64) (int64, error) {
|
||||
if nMax < 1 {
|
||||
return 0, ErrInvalidMaxInt
|
||||
}
|
||||
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(max))
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(nMax))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -2,16 +2,25 @@ package rands_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/jimeh/rands"
|
||||
)
|
||||
|
||||
func ExampleInt() {
|
||||
n, _ := rands.Int(2147483647)
|
||||
fmt.Printf("%d\n", n) // => 1334400235
|
||||
n, err := rands.Int(2147483647)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%d\n", n) // => 1908357440
|
||||
}
|
||||
|
||||
func ExampleInt64() {
|
||||
n, _ := rands.Int64(int64(9223372036854775807))
|
||||
fmt.Printf("%d\n", n) // => 8256935979116161233
|
||||
n, err := rands.Int64(int64(9223372036854775807))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%d\n", n) // => 6530460062499341591
|
||||
}
|
||||
|
||||
31
ints_test.go
31
ints_test.go
@@ -1,7 +1,6 @@
|
||||
package rands
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -16,67 +15,67 @@ var testIntCases = []struct {
|
||||
{
|
||||
name: "n=-2394345",
|
||||
max: -2394345,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-409600",
|
||||
max: -409600,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-1024",
|
||||
max: -1024,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-128",
|
||||
max: -128,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-32",
|
||||
max: -32,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-16",
|
||||
max: -16,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-8",
|
||||
max: -8,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-7",
|
||||
max: -7,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-2",
|
||||
max: -2,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-1",
|
||||
max: -1,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=0",
|
||||
max: 0,
|
||||
errIs: errInvalidMaxInt,
|
||||
errIs: ErrInvalidMaxInt,
|
||||
errStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{name: "n=1", max: 1},
|
||||
@@ -92,6 +91,8 @@ var testIntCases = []struct {
|
||||
}
|
||||
|
||||
func TestInt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range testIntCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Int(tt.max)
|
||||
@@ -102,7 +103,7 @@ func TestInt(t *testing.T) {
|
||||
}
|
||||
|
||||
if tt.errIs != nil {
|
||||
assert.True(t, errors.Is(err, errInvalidMaxInt))
|
||||
assert.ErrorIs(t, err, tt.errIs)
|
||||
}
|
||||
|
||||
if tt.errStr != "" {
|
||||
@@ -123,6 +124,8 @@ func BenchmarkInt(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestInt64(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range testIntCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Int64(int64(tt.max))
|
||||
@@ -133,7 +136,7 @@ func TestInt64(t *testing.T) {
|
||||
}
|
||||
|
||||
if tt.errIs != nil {
|
||||
assert.True(t, errors.Is(err, errInvalidMaxInt))
|
||||
assert.ErrorIs(t, err, tt.errIs)
|
||||
}
|
||||
|
||||
if tt.errStr != "" {
|
||||
|
||||
9
rands.go
9
rands.go
@@ -4,8 +4,15 @@
|
||||
//
|
||||
// All functions which produce strings from a alphabet of characters uses
|
||||
// rand.Int() to ensure a uniform distribution of all possible values.
|
||||
//
|
||||
// rands is intended for use in production code where random data generation is
|
||||
// required. All functions have a error return value, which should be
|
||||
// checked.
|
||||
//
|
||||
// For tests there is the randsmust package, which has all the same functions
|
||||
// but with single return values, and they panic in the event of an error.
|
||||
package rands
|
||||
|
||||
import "errors"
|
||||
|
||||
var errBase = errors.New("rands")
|
||||
var Err = errors.New("rands")
|
||||
|
||||
13
randsmust/bytes.go
Normal file
13
randsmust/bytes.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package randsmust
|
||||
|
||||
import "github.com/jimeh/rands"
|
||||
|
||||
// Bytes generates a byte slice of n number of random bytes.
|
||||
func Bytes(n int) []byte {
|
||||
r, err := rands.Bytes(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
12
randsmust/bytes_example_test.go
Normal file
12
randsmust/bytes_example_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package randsmust_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jimeh/rands/randsmust"
|
||||
)
|
||||
|
||||
func ExampleBytes() {
|
||||
b := randsmust.Bytes(8)
|
||||
fmt.Printf("%+v\n", b) // => [6 99 106 54 163 188 28 152]
|
||||
}
|
||||
19
randsmust/bytes_test.go
Normal file
19
randsmust/bytes_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package randsmust
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Bytes(tt.n)
|
||||
|
||||
assert.Len(t, got, tt.n)
|
||||
})
|
||||
}
|
||||
}
|
||||
23
randsmust/ints.go
Normal file
23
randsmust/ints.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package randsmust
|
||||
|
||||
import "github.com/jimeh/rands"
|
||||
|
||||
// Int generates a random int ranging between 0 and nMax.
|
||||
func Int(nMax int) int {
|
||||
r, err := rands.Int(nMax)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Int64 generates a random int64 ranging between 0 and nMax.
|
||||
func Int64(nMax int64) int64 {
|
||||
r, err := rands.Int64(nMax)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
17
randsmust/ints_example_test.go
Normal file
17
randsmust/ints_example_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package randsmust_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jimeh/rands/randsmust"
|
||||
)
|
||||
|
||||
func ExampleInt() {
|
||||
n := randsmust.Int(2147483647)
|
||||
fmt.Printf("%d\n", n) // => 1616989970
|
||||
}
|
||||
|
||||
func ExampleInt64() {
|
||||
n := randsmust.Int64(int64(9223372036854775807))
|
||||
fmt.Printf("%d\n", n) // => 1599573251306894157
|
||||
}
|
||||
144
randsmust/ints_test.go
Normal file
144
randsmust/ints_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package randsmust
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/rands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var testIntCases = []struct {
|
||||
name string
|
||||
max int
|
||||
panicErrIs error
|
||||
panicStr string
|
||||
}{
|
||||
{
|
||||
name: "n=-2394345",
|
||||
max: -2394345,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-409600",
|
||||
max: -409600,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-1024",
|
||||
max: -1024,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-128",
|
||||
max: -128,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-32",
|
||||
max: -32,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-16",
|
||||
max: -16,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-8",
|
||||
max: -8,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-7",
|
||||
max: -7,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-2",
|
||||
max: -2,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=-1",
|
||||
max: -1,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{
|
||||
name: "n=0",
|
||||
max: 0,
|
||||
panicErrIs: rands.ErrInvalidMaxInt,
|
||||
panicStr: "rands: max cannot be less than 1",
|
||||
},
|
||||
{name: "n=1", max: 1},
|
||||
{name: "n=2", max: 2},
|
||||
{name: "n=7", max: 7},
|
||||
{name: "n=8", max: 8},
|
||||
{name: "n=16", max: 16},
|
||||
{name: "n=32", max: 32},
|
||||
{name: "n=128", max: 128},
|
||||
{name: "n=1024", max: 1024},
|
||||
{name: "n=409600", max: 409600},
|
||||
{name: "n=2394345", max: 2394345},
|
||||
}
|
||||
|
||||
func TestInt(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range testIntCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got int
|
||||
p := recoverPanic(func() {
|
||||
got = Int(tt.max)
|
||||
})
|
||||
|
||||
if tt.panicErrIs == nil || tt.panicStr == "" {
|
||||
assert.GreaterOrEqual(t, got, 0)
|
||||
assert.LessOrEqual(t, got, tt.max)
|
||||
}
|
||||
|
||||
if tt.panicErrIs != nil {
|
||||
assert.ErrorIs(t, p.(error), tt.panicErrIs)
|
||||
}
|
||||
|
||||
if tt.panicStr != "" {
|
||||
assert.EqualError(t, p.(error), tt.panicStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInt64(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range testIntCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got int64
|
||||
p := recoverPanic(func() {
|
||||
got = Int64(int64(tt.max))
|
||||
})
|
||||
|
||||
if tt.panicErrIs == nil || tt.panicStr == "" {
|
||||
assert.GreaterOrEqual(t, got, int64(0))
|
||||
assert.LessOrEqual(t, got, int64(tt.max))
|
||||
}
|
||||
|
||||
if tt.panicErrIs != nil {
|
||||
assert.ErrorIs(t, p.(error), tt.panicErrIs)
|
||||
}
|
||||
|
||||
if tt.panicStr != "" {
|
||||
assert.EqualError(t, p.(error), tt.panicStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
15
randsmust/randsmust.go
Normal file
15
randsmust/randsmust.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Package randsmust provides a suite of functions that use crypto/rand to
|
||||
// generate cryptographically secure random strings in various formats, as well
|
||||
// as ints and bytes.
|
||||
//
|
||||
// All functions which produce strings from a alphabet of characters uses
|
||||
// rand.Int() to ensure a uniform distribution of all possible values.
|
||||
//
|
||||
// randsmust is specifically intended as an alternative to rands for use in
|
||||
// tests. All functions return a single value, and panic in the event of an
|
||||
// error. This makes them easy to use when building structs in test cases that
|
||||
// need random data.
|
||||
//
|
||||
// For production code, make sure to use the rands package and check returned
|
||||
// errors.
|
||||
package randsmust
|
||||
30
randsmust/randsmust_test.go
Normal file
30
randsmust/randsmust_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package randsmust
|
||||
|
||||
var testCases = []struct {
|
||||
name string
|
||||
n int
|
||||
}{
|
||||
{name: "n=0", n: 0},
|
||||
{name: "n=1", n: 1},
|
||||
{name: "n=2", n: 2},
|
||||
{name: "n=7", n: 7},
|
||||
{name: "n=8", n: 8},
|
||||
{name: "n=16", n: 16},
|
||||
{name: "n=32", n: 32},
|
||||
{name: "n=128", n: 128},
|
||||
{name: "n=1024", n: 1024},
|
||||
{name: "n=409600", n: 409600},
|
||||
{name: "n=2394345", n: 2394345},
|
||||
}
|
||||
|
||||
func recoverPanic(f func()) (p interface{}) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
p = r
|
||||
}
|
||||
}()
|
||||
|
||||
f()
|
||||
|
||||
return
|
||||
}
|
||||
32
randsmust/shuffle.go
Normal file
32
randsmust/shuffle.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package randsmust
|
||||
|
||||
import "github.com/jimeh/rands"
|
||||
|
||||
// Shuffle randomizes the order of a collection of n elements using
|
||||
// cryptographically secure random values from crypto/rand. It implements the
|
||||
// Fisher-Yates shuffle algorithm.
|
||||
//
|
||||
// The swap function is called to exchange values at indices i and j. This
|
||||
// signature is compatible with Shuffle from math/rand and math/rand/v2 for easy
|
||||
// migration.
|
||||
//
|
||||
// If an error occurs during shuffling, this function will panic.
|
||||
func Shuffle(n int, swap func(i, j int)) {
|
||||
err := rands.Shuffle(n, swap)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ShuffleSlice randomizes the order of elements in a slice in-place using
|
||||
// cryptographically secure random values from crypto/rand.
|
||||
//
|
||||
// It implements the Fisher-Yates shuffle algorithm.
|
||||
//
|
||||
// If an error occurs during shuffling, this function will panic.
|
||||
func ShuffleSlice[T any](slice []T) {
|
||||
err := rands.ShuffleSlice(slice)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
25
randsmust/shuffle_example_test.go
Normal file
25
randsmust/shuffle_example_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package randsmust_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jimeh/rands/randsmust"
|
||||
)
|
||||
|
||||
func ExampleShuffle() {
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
|
||||
randsmust.Shuffle(len(numbers), func(i, j int) {
|
||||
numbers[i], numbers[j] = numbers[j], numbers[i]
|
||||
})
|
||||
|
||||
fmt.Println(numbers) // => [4 2 5 3 1]
|
||||
}
|
||||
|
||||
func ExampleShuffleSlice() {
|
||||
mixed := []any{1, "two", 3.14, true, nil}
|
||||
|
||||
randsmust.ShuffleSlice(mixed)
|
||||
|
||||
fmt.Println(mixed) // => [two <nil> true 3.14 1]
|
||||
}
|
||||
456
randsmust/shuffle_test.go
Normal file
456
randsmust/shuffle_test.go
Normal file
@@ -0,0 +1,456 @@
|
||||
package randsmust
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/jimeh/rands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func factorial(n int) int {
|
||||
factorial := 1
|
||||
for i := 1; i <= n; i++ {
|
||||
factorial *= i
|
||||
}
|
||||
|
||||
return factorial
|
||||
}
|
||||
|
||||
func TestShuffle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("n < 0", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := recoverPanic(func() {
|
||||
Shuffle(-1, func(_, _ int) {})
|
||||
})
|
||||
|
||||
require.NotNil(t, p, "Expected a panic")
|
||||
assert.ErrorIs(t, p.(error), rands.ErrInvalidShuffleNegativeN)
|
||||
assert.ErrorIs(t, p.(error), rands.ErrShuffle)
|
||||
assert.ErrorIs(t, p.(error), rands.Err)
|
||||
})
|
||||
|
||||
t.Run("n == 0", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
swapCount := 0
|
||||
p := recoverPanic(func() {
|
||||
Shuffle(0, func(_, _ int) {
|
||||
swapCount++
|
||||
})
|
||||
})
|
||||
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
assert.Equal(t, 0, swapCount)
|
||||
})
|
||||
|
||||
t.Run("n == 1", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
swapCount := 0
|
||||
p := recoverPanic(func() {
|
||||
Shuffle(1, func(_, _ int) {
|
||||
swapCount++
|
||||
})
|
||||
})
|
||||
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
assert.Equal(t, 0, swapCount)
|
||||
})
|
||||
|
||||
t.Run("n == 2", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
swapCount := 0
|
||||
p := recoverPanic(func() {
|
||||
Shuffle(2, func(_, _ int) {
|
||||
swapCount++
|
||||
})
|
||||
})
|
||||
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
assert.Equal(t, 1, swapCount)
|
||||
})
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
arr := make([]int, 100)
|
||||
for i := range arr {
|
||||
arr[i] = i
|
||||
}
|
||||
|
||||
arrCopy := make([]int, len(arr))
|
||||
copy(arrCopy, arr)
|
||||
|
||||
p := recoverPanic(func() {
|
||||
Shuffle(len(arr), func(i, j int) {
|
||||
arr[i], arr[j] = arr[j], arr[i]
|
||||
})
|
||||
})
|
||||
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
assert.NotEqual(t, arrCopy, arr, "Shuffle did not change the array")
|
||||
assert.ElementsMatch(t, arrCopy, arr, "Shuffle changed elements")
|
||||
})
|
||||
|
||||
t.Run("swaps", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
swapSame := 0
|
||||
swapDifferent := 0
|
||||
arr := make([]int, 100)
|
||||
for j := range arr {
|
||||
arr[j] = j
|
||||
}
|
||||
|
||||
p := recoverPanic(func() {
|
||||
Shuffle(len(arr), func(i, j int) {
|
||||
if i == j {
|
||||
swapSame++
|
||||
} else {
|
||||
swapDifferent++
|
||||
}
|
||||
|
||||
arr[i], arr[j] = arr[j], arr[i]
|
||||
})
|
||||
})
|
||||
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
|
||||
// Fisher-Yates with n elements should make exactly n-1 swaps
|
||||
assert.Equal(t, len(arr)-1, swapSame+swapDifferent,
|
||||
"Unexpected swaps count",
|
||||
)
|
||||
|
||||
// Ensure we have more different-element swaps than self-swaps. The
|
||||
// lower the input shuffle n value, the more likely this assertion will
|
||||
// fail. For a n=100 shuffle, this is exceptionally unlikely to fail.
|
||||
assert.Greater(t, swapDifferent, swapSame,
|
||||
"Expected more different-element swaps than self-swaps",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("swap ranges", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
n := 32
|
||||
runs := 1000
|
||||
|
||||
for run := 0; run < runs; run++ {
|
||||
called := 0
|
||||
p := recoverPanic(func() {
|
||||
Shuffle(n, func(i, j int) {
|
||||
called++
|
||||
|
||||
// Verify indices are in bounds.
|
||||
assert.True(t,
|
||||
i >= 0 && i < n, "Out of bounds index i = %d", i,
|
||||
)
|
||||
assert.True(t,
|
||||
j >= 0 && j < n, "Out of bounds index j = %d", j,
|
||||
)
|
||||
|
||||
// For Fisher-Yates, i should be > 0 and j should be in
|
||||
// range [0,i].
|
||||
assert.Greater(t, i, 0, "Expected i > 0, got i=%d", i)
|
||||
assert.True(t,
|
||||
j >= 0 && j <= i,
|
||||
"Expected j in range [0,%d], got j=%d", i, j,
|
||||
)
|
||||
})
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
|
||||
// Fisher-Yates with n elements should make exactly n-1 swaps
|
||||
expected := n - 1
|
||||
assert.Equal(t, expected, called,
|
||||
"Expected %d swap calls, got %d", expected, called,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("all permutations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Use a small array of 5 elements to make it feasible to track all
|
||||
// permutations.
|
||||
n := 5
|
||||
fact := factorial(n) // 120
|
||||
runs := fact * 3000 // 360000
|
||||
|
||||
permCounts := make(map[string]int)
|
||||
for i := 0; i < runs; i++ {
|
||||
arr := make([]int, n)
|
||||
for i := range arr {
|
||||
arr[i] = i
|
||||
}
|
||||
|
||||
p := recoverPanic(func() {
|
||||
Shuffle(len(arr), func(i, j int) {
|
||||
arr[i], arr[j] = arr[j], arr[i]
|
||||
})
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
|
||||
// Convert the permutation to a string key and count it.
|
||||
key := fmt.Sprintf("%v", arr)
|
||||
permCounts[key]++
|
||||
}
|
||||
|
||||
assert.Equal(t, fact, len(permCounts),
|
||||
"Expected %d different permutations", fact,
|
||||
)
|
||||
|
||||
wantCount := float64(runs) / float64(fact)
|
||||
margin := 0.15
|
||||
minAcceptable := int(wantCount * (1 - margin))
|
||||
maxAcceptable := int(wantCount * (1 + margin))
|
||||
|
||||
for perm, count := range permCounts {
|
||||
assert.True(t,
|
||||
count >= minAcceptable && count <= maxAcceptable,
|
||||
"Non-uniform distribution for %s: count=%d, expected=%v±%v",
|
||||
perm, count, wantCount, wantCount*margin,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("distribution", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Track which positions received which random indices
|
||||
n := 100
|
||||
posCounts := make([]map[int]int, n)
|
||||
for i := range posCounts {
|
||||
posCounts[i] = make(map[int]int)
|
||||
}
|
||||
|
||||
runs := 3000
|
||||
for run := 0; run < runs; run++ {
|
||||
p := recoverPanic(func() {
|
||||
Shuffle(n, func(i, j int) {
|
||||
posCounts[i][j]++
|
||||
})
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
}
|
||||
|
||||
// For each position, check that it received a reasonable distribution.
|
||||
for i := n - 1; i >= n-len(posCounts); i-- {
|
||||
// Calculate how many unique positions we should expect.
|
||||
// Position i should receive random positions from 0 to i, and
|
||||
// allow for some statistical variation.
|
||||
want := int(float64(i+1) * 0.9)
|
||||
assert.GreaterOrEqual(t,
|
||||
len(posCounts[i]), want,
|
||||
"Position %d: expected ~%d unique indices, got %d",
|
||||
i, want, len(posCounts[i]),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestShuffleSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("empty slice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slice := []int{}
|
||||
p := recoverPanic(func() {
|
||||
ShuffleSlice(slice)
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
assert.Empty(t, slice)
|
||||
})
|
||||
|
||||
t.Run("single element", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slice := []int{42}
|
||||
origSlice := make([]int, len(slice))
|
||||
copy(origSlice, slice)
|
||||
|
||||
p := recoverPanic(func() {
|
||||
ShuffleSlice(slice)
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
|
||||
assert.Equal(t,
|
||||
origSlice, slice, "Single element slice should remain unchanged",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("two elements", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slice := []int{1, 2}
|
||||
origSlice := make([]int, len(slice))
|
||||
copy(origSlice, slice)
|
||||
|
||||
p := recoverPanic(func() {
|
||||
ShuffleSlice(slice)
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
|
||||
// With two elements, the slice might remain the same or be swapped
|
||||
assert.Len(t, slice, len(origSlice))
|
||||
assert.ElementsMatch(t, origSlice, slice)
|
||||
})
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slice := make([]int, 100)
|
||||
for i := range slice {
|
||||
slice[i] = i
|
||||
}
|
||||
|
||||
sliceCopy := make([]int, len(slice))
|
||||
copy(sliceCopy, slice)
|
||||
|
||||
p := recoverPanic(func() {
|
||||
ShuffleSlice(slice)
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
|
||||
assert.NotEqual(t,
|
||||
sliceCopy, slice, "ShuffleSlice did not change the slice",
|
||||
)
|
||||
assert.ElementsMatch(t,
|
||||
sliceCopy, slice, "ShuffleSlice changed elements",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("string slice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
strSlice := []string{"a", "b", "c", "d", "e"}
|
||||
strCopy := make([]string, len(strSlice))
|
||||
copy(strCopy, strSlice)
|
||||
|
||||
p := recoverPanic(func() {
|
||||
ShuffleSlice(strSlice)
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
|
||||
assert.ElementsMatch(t, strCopy, strSlice)
|
||||
})
|
||||
|
||||
t.Run("struct slice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testStruct struct {
|
||||
id int
|
||||
name string
|
||||
}
|
||||
structSlice := []testStruct{
|
||||
{1, "one"},
|
||||
{2, "two"},
|
||||
{3, "three"},
|
||||
{4, "four"},
|
||||
}
|
||||
structCopy := make([]testStruct, len(structSlice))
|
||||
copy(structCopy, structSlice)
|
||||
|
||||
p := recoverPanic(func() {
|
||||
ShuffleSlice(structSlice)
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
assert.ElementsMatch(t, structCopy, structSlice)
|
||||
})
|
||||
|
||||
t.Run("all permutations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Use a small slice of 5 elements to make it feasible to track all
|
||||
// permutations.
|
||||
n := 5
|
||||
fact := factorial(n) // 120
|
||||
runs := fact * 3000 // 360000
|
||||
|
||||
permCounts := make(map[string]int)
|
||||
for i := 0; i < runs; i++ {
|
||||
slice := make([]int, n)
|
||||
for j := range slice {
|
||||
slice[j] = j
|
||||
}
|
||||
|
||||
p := recoverPanic(func() {
|
||||
ShuffleSlice(slice)
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
|
||||
// Convert the permutation to a string key and count it.
|
||||
key := fmt.Sprintf("%v", slice)
|
||||
permCounts[key]++
|
||||
}
|
||||
|
||||
assert.Equal(t, fact, len(permCounts),
|
||||
"Expected %d different permutations", fact,
|
||||
)
|
||||
|
||||
wantCount := float64(runs) / float64(fact)
|
||||
margin := 0.15
|
||||
minAcceptable := int(wantCount * (1 - margin))
|
||||
maxAcceptable := int(wantCount * (1 + margin))
|
||||
|
||||
for perm, count := range permCounts {
|
||||
assert.True(t,
|
||||
count >= minAcceptable && count <= maxAcceptable,
|
||||
"Non-uniform distribution for %s: count=%d, expected=%v±%v",
|
||||
perm, count, wantCount, wantCount*margin,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("distribution", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Track where each original index ends up after shuffling
|
||||
n := 100
|
||||
// posCounts[originalPos][newPos] tracks how many times
|
||||
// the element originally at position i ended up at position j
|
||||
posCounts := make([]map[int]int, n)
|
||||
for i := range posCounts {
|
||||
posCounts[i] = make(map[int]int)
|
||||
}
|
||||
|
||||
runs := 3000
|
||||
for run := 0; run < runs; run++ {
|
||||
// Create a slice where the value is its original position
|
||||
slice := make([]int, n)
|
||||
for i := range slice {
|
||||
slice[i] = i
|
||||
}
|
||||
|
||||
p := recoverPanic(func() {
|
||||
ShuffleSlice(slice)
|
||||
})
|
||||
require.Nil(t, p, "Did not expect a panic")
|
||||
|
||||
// Track where each original position ended up
|
||||
for newPos, origPos := range slice {
|
||||
posCounts[origPos][newPos]++
|
||||
}
|
||||
}
|
||||
|
||||
// For each original position, check that it was distributed
|
||||
// reasonably across all possible new positions
|
||||
for i := n - 1; i >= n-len(posCounts); i-- {
|
||||
// Calculate how many unique positions we should expect.
|
||||
// Position i should receive random positions from 0 to i, and
|
||||
// allow for some statistical variation.
|
||||
want := int(float64(i+1) * 0.9)
|
||||
assert.GreaterOrEqual(t,
|
||||
len(posCounts[i]), want,
|
||||
"Original position %d: expected ~%d unique positions, got %d",
|
||||
i, want, len(posCounts[i]),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
213
randsmust/strings.go
Normal file
213
randsmust/strings.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package randsmust
|
||||
|
||||
import (
|
||||
"github.com/jimeh/rands"
|
||||
)
|
||||
|
||||
// Base64 generates a random base64 encoded string of n number of bytes.
|
||||
//
|
||||
// Length of the returned string is about one third greater than the value of n,
|
||||
// and it may contain characters A-Z, a-z, 0-9, "+", "/", and "=".
|
||||
func Base64(n int) string {
|
||||
r, err := rands.Base64(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Base64URL generates a URL-safe un-padded random base64 encoded string of n
|
||||
// number of bytes.
|
||||
//
|
||||
// Length of the returned string is about one third greater than the value of n,
|
||||
// and it may contain characters A-Z, a-z, 0-9, "-", and "_".
|
||||
func Base64URL(n int) string {
|
||||
r, err := rands.Base64URL(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Hex generates a random hexadecimal encoded string of n number of bytes.
|
||||
//
|
||||
// Length of the returned string is twice the value of n, and it may contain
|
||||
// characters 0-9 and a-f.
|
||||
func Hex(n int) string {
|
||||
r, err := rands.Hex(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Alphanumeric generates a random alphanumeric string of n length.
|
||||
//
|
||||
// The returned string may contain A-Z, a-z, and 0-9.
|
||||
func Alphanumeric(n int) string {
|
||||
r, err := rands.Alphanumeric(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Alphabetic generates a random alphabetic string of n length.
|
||||
//
|
||||
// The returned string may contain A-Z, and a-z.
|
||||
func Alphabetic(n int) string {
|
||||
r, err := rands.Alphabetic(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Numeric generates a random numeric string of n length.
|
||||
//
|
||||
// The returned string may contain 0-9.
|
||||
func Numeric(n int) string {
|
||||
r, err := rands.Numeric(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Upper generates a random uppercase alphabetic string of n length.
|
||||
//
|
||||
// The returned string may contain A-Z.
|
||||
func Upper(n int) string {
|
||||
r, err := rands.Upper(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// UpperNumeric generates a random uppercase alphanumeric string of n length.
|
||||
//
|
||||
// The returned string may contain A-Z and 0-9.
|
||||
func UpperNumeric(n int) string {
|
||||
r, err := rands.UpperNumeric(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Lower generates a random lowercase alphabetic string of n length.
|
||||
//
|
||||
// The returned string may contain a-z.
|
||||
func Lower(n int) string {
|
||||
r, err := rands.Lower(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// LowerNumeric generates a random lowercase alphanumeric string of n length.
|
||||
//
|
||||
// The returned string may contain A-Z and 0-9.
|
||||
func LowerNumeric(n int) string {
|
||||
r, err := rands.LowerNumeric(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// String generates a random string of n length using the given ASCII alphabet.
|
||||
//
|
||||
// The specified alphabet determines what characters are used in the returned
|
||||
// random string. The alphabet can only contain ASCII characters, use
|
||||
// UnicodeString() if you need a alphabet with Unicode characters.
|
||||
func String(n int, alphabet string) string {
|
||||
r, err := rands.String(n, alphabet)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// UnicodeString generates a random string of n length using the given Unicode
|
||||
// alphabet.
|
||||
//
|
||||
// The specified alphabet determines what characters are used in the returned
|
||||
// random string. The length of the returned string will be n or greater
|
||||
// depending on the byte-length of characters which were randomly selected from
|
||||
// the alphabet.
|
||||
func UnicodeString(n int, alphabet []rune) string {
|
||||
r, err := rands.UnicodeString(n, alphabet)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// DNSLabel returns a random string of n length in a DNS label compliant format
|
||||
// as defined in RFC 1035, section 2.3.1.
|
||||
//
|
||||
// It also adheres to RFC 5891, section 4.2.3.1.
|
||||
//
|
||||
// In summary, the generated random string will:
|
||||
//
|
||||
// - be between 1 and 63 characters in length, other n values returns a error
|
||||
// - first character will be one of a-z
|
||||
// - last character will be one of a-z or 0-9
|
||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||
func DNSLabel(n int) string {
|
||||
r, err := rands.DNSLabel(n)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// UUIDv4 returns a random UUID v4 in string format as defined by RFC 4122,
|
||||
// section 4.4.
|
||||
func UUID() string {
|
||||
r, err := rands.UUID()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// UUIDv7 returns a time-ordered UUID v7 in string format.
|
||||
//
|
||||
// The UUID v7 format uses a timestamp with millisecond precision in the most
|
||||
// significant bits, followed by random data. This provides both uniqueness and
|
||||
// chronological ordering, making it ideal for database primary keys and
|
||||
// situations where sorting by creation time is desired.
|
||||
//
|
||||
// References:
|
||||
// - https://uuid7.com/
|
||||
// - https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
|
||||
//
|
||||
//nolint:lll
|
||||
func UUIDv7() string {
|
||||
r, err := rands.UUIDv7()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
82
randsmust/strings_example_test.go
Normal file
82
randsmust/strings_example_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package randsmust_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jimeh/rands/randsmust"
|
||||
)
|
||||
|
||||
func ExampleBase64() {
|
||||
s := randsmust.Base64(16)
|
||||
fmt.Println(s) // => rGnZOxJunCd5h+piBpOfDA==
|
||||
}
|
||||
|
||||
func ExampleBase64URL() {
|
||||
s := randsmust.Base64URL(16)
|
||||
fmt.Println(s) // => NlXKmutou2knLU8q7Hlp5Q
|
||||
}
|
||||
|
||||
func ExampleHex() {
|
||||
s := randsmust.Hex(16)
|
||||
fmt.Println(s) // => 1013ec67a802be177d3e37f46951e97f
|
||||
}
|
||||
|
||||
func ExampleAlphanumeric() {
|
||||
s := randsmust.Alphanumeric(16)
|
||||
fmt.Println(s) // => mjT119HdPslVfvUE
|
||||
}
|
||||
|
||||
func ExampleAlphabetic() {
|
||||
s := randsmust.Alphabetic(16)
|
||||
fmt.Println(s) // => RLaRaTVqcrxvNkiz
|
||||
}
|
||||
|
||||
func ExampleUpper() {
|
||||
s := randsmust.Upper(16)
|
||||
fmt.Println(s) // => CANJDLMHANPQNXUE
|
||||
}
|
||||
|
||||
func ExampleUpperNumeric() {
|
||||
s := randsmust.UpperNumeric(16)
|
||||
fmt.Println(s) // => EERZHC96KOIRU9DM
|
||||
}
|
||||
|
||||
func ExampleLower() {
|
||||
s := randsmust.Lower(16)
|
||||
fmt.Println(s) // => aoybqdwigyezucjy
|
||||
}
|
||||
|
||||
func ExampleLowerNumeric() {
|
||||
s := randsmust.LowerNumeric(16)
|
||||
fmt.Println(s) // => hs8l2l0750med3g2
|
||||
}
|
||||
|
||||
func ExampleNumeric() {
|
||||
s := randsmust.Numeric(16)
|
||||
fmt.Println(s) // => 3126402104379869
|
||||
}
|
||||
|
||||
func ExampleString() {
|
||||
s := randsmust.String(16, "abcdefABCDEF")
|
||||
fmt.Println(s) // => cbdCAbABECaADcaB
|
||||
}
|
||||
|
||||
func ExampleUnicodeString() {
|
||||
s := randsmust.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大"))
|
||||
fmt.Println(s) // => 下夕七下千千力入八三力夕千三土七
|
||||
}
|
||||
|
||||
func ExampleDNSLabel() {
|
||||
s := randsmust.DNSLabel(16)
|
||||
fmt.Println(s) // => urqkt-remuwz5083
|
||||
}
|
||||
|
||||
func ExampleUUID() {
|
||||
s := randsmust.UUID()
|
||||
fmt.Println(s) // => 5baa35a6-9a46-49b4-91d0-9530173e118d
|
||||
}
|
||||
|
||||
func ExampleUUIDv7() {
|
||||
s := randsmust.UUIDv7()
|
||||
fmt.Println(s) // => 01954a3a-a06f-7186-8774-51a770503eb2
|
||||
}
|
||||
622
randsmust/strings_test.go
Normal file
622
randsmust/strings_test.go
Normal file
@@ -0,0 +1,622 @@
|
||||
package randsmust
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jimeh/rands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "0123456789abcdef"
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Hex(tt.n)
|
||||
|
||||
assert.Len(t, got, tt.n*2)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBase64(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
|
||||
"0123456789+/="
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Base64(tt.n)
|
||||
|
||||
b, err := base64.StdEncoding.DecodeString(got)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, b, tt.n)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBase64URL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
|
||||
"0123456789-_"
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Base64URL(tt.n)
|
||||
|
||||
b, err := base64.RawURLEncoding.DecodeString(got)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, b, tt.n)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphanumeric(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Alphanumeric(tt.n)
|
||||
|
||||
assert.Len(t, got, tt.n)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlphabetic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Alphabetic(tt.n)
|
||||
|
||||
assert.Len(t, got, tt.n)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumeric(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "0123456789"
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Numeric(tt.n)
|
||||
|
||||
assert.Len(t, got, tt.n)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpper(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Upper(tt.n)
|
||||
|
||||
assert.Len(t, got, tt.n)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpperNumeric(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := UpperNumeric(tt.n)
|
||||
|
||||
assert.Len(t, got, tt.n)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLower(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Lower(tt.n)
|
||||
|
||||
assert.Len(t, got, tt.n)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLowerNumeric(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := LowerNumeric(tt.n)
|
||||
|
||||
assert.Len(t, got, tt.n)
|
||||
assertAllowedChars(t, allowed, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var stringTestCases = []struct {
|
||||
name string
|
||||
n int
|
||||
alphabet string
|
||||
panicErrIs error
|
||||
panicStr string
|
||||
}{
|
||||
{
|
||||
name: "greek",
|
||||
n: 32,
|
||||
alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" +
|
||||
"άίόύώέϊϋΐΰΆΈΌΏΎΊ",
|
||||
panicErrIs: rands.ErrNonASCIIAlphabet,
|
||||
panicStr: "rands: alphabet contains non-ASCII characters",
|
||||
},
|
||||
{
|
||||
name: "chinese",
|
||||
n: 32,
|
||||
alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" +
|
||||
"时道出而要于就下得可你年生",
|
||||
panicErrIs: rands.ErrNonASCIIAlphabet,
|
||||
panicStr: "rands: alphabet contains non-ASCII characters",
|
||||
},
|
||||
{
|
||||
name: "japanese",
|
||||
n: 32,
|
||||
alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" +
|
||||
"手文日月木水火犬王正出本右四",
|
||||
panicErrIs: rands.ErrNonASCIIAlphabet,
|
||||
panicStr: "rands: alphabet contains non-ASCII characters",
|
||||
},
|
||||
{
|
||||
name: "n=0",
|
||||
n: 0,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=1",
|
||||
n: 1,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=2",
|
||||
n: 2,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=7",
|
||||
n: 7,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=8",
|
||||
n: 8,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=16",
|
||||
n: 16,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=32",
|
||||
n: 32,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=128",
|
||||
n: 128,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=1024",
|
||||
n: 1024,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=409600",
|
||||
n: 409600,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "n=2394345",
|
||||
n: 2394345,
|
||||
alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-",
|
||||
},
|
||||
{
|
||||
name: "uppercase",
|
||||
n: 16,
|
||||
alphabet: "ABCDEFGHJKMNPRSTUVWXYZ",
|
||||
},
|
||||
{
|
||||
name: "lowercase",
|
||||
n: 16,
|
||||
alphabet: "abcdefghjkmnprstuvwxyz",
|
||||
},
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range stringTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
p := recoverPanic(func() {
|
||||
got = String(tt.n, tt.alphabet)
|
||||
})
|
||||
|
||||
if tt.panicErrIs == nil || tt.panicStr == "" {
|
||||
assert.Len(t, []rune(got), tt.n)
|
||||
assertAllowedChars(t, tt.alphabet, got)
|
||||
}
|
||||
|
||||
if tt.panicErrIs != nil {
|
||||
assert.ErrorIs(t, p.(error), tt.panicErrIs)
|
||||
}
|
||||
|
||||
if tt.panicStr != "" {
|
||||
assert.EqualError(t, p.(error), tt.panicStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var unicodeStringTestCases = []struct {
|
||||
name string
|
||||
n int
|
||||
alphabet string
|
||||
}{
|
||||
{
|
||||
name: "n=0",
|
||||
n: 0,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=1",
|
||||
n: 1,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=2",
|
||||
n: 2,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=7",
|
||||
n: 7,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=8",
|
||||
n: 8,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=16",
|
||||
n: 16,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=32",
|
||||
n: 32,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=128",
|
||||
n: 128,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=1024",
|
||||
n: 1024,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=409600",
|
||||
n: 409600,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "n=2394345",
|
||||
n: 2394345,
|
||||
alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" +
|
||||
"手文日月木水火犬王正出本右",
|
||||
},
|
||||
{
|
||||
name: "latin",
|
||||
n: 32,
|
||||
alphabet: "ABCDEFGHJKMNPRSTUVWXYZabcdefghjkmnprstuvwxyz",
|
||||
},
|
||||
{
|
||||
name: "greek",
|
||||
n: 32,
|
||||
alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" +
|
||||
"άίόύώέϊϋΐΰΆΈΌΏΎΊ",
|
||||
},
|
||||
{
|
||||
name: "chinese",
|
||||
n: 32,
|
||||
alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" +
|
||||
"时道出而要于就下得可你年生",
|
||||
},
|
||||
{
|
||||
name: "japanese",
|
||||
n: 32,
|
||||
alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" +
|
||||
"手文日月木水火犬王正出本右四",
|
||||
},
|
||||
}
|
||||
|
||||
func TestUnicodeString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range unicodeStringTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := UnicodeString(tt.n, []rune(tt.alphabet))
|
||||
|
||||
assert.Len(t, []rune(got), tt.n)
|
||||
assertAllowedChars(t, tt.alphabet, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var dnsLabelTestCases = []struct {
|
||||
name string
|
||||
n int
|
||||
panicErrIs error
|
||||
panicStr string
|
||||
}{
|
||||
{
|
||||
name: "n=-128",
|
||||
n: -128,
|
||||
panicErrIs: rands.ErrDNSLabelLength,
|
||||
panicStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
{
|
||||
name: "n=0",
|
||||
n: 0,
|
||||
panicErrIs: rands.ErrDNSLabelLength,
|
||||
panicStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
{name: "n=1", n: 1},
|
||||
{name: "n=2", n: 2},
|
||||
{name: "n=3", n: 3},
|
||||
{name: "n=4", n: 4},
|
||||
{name: "n=5", n: 5},
|
||||
{name: "n=6", n: 6},
|
||||
{name: "n=7", n: 7},
|
||||
{name: "n=8", n: 8},
|
||||
{name: "n=16", n: 16},
|
||||
{name: "n=32", n: 32},
|
||||
{name: "n=63", n: 63},
|
||||
{
|
||||
name: "n=64",
|
||||
n: 64,
|
||||
panicErrIs: rands.ErrDNSLabelLength,
|
||||
panicStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
{
|
||||
name: "n=128",
|
||||
n: 128,
|
||||
panicErrIs: rands.ErrDNSLabelLength,
|
||||
panicStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
}
|
||||
|
||||
func TestDNSLabel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range dnsLabelTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// generate lots of labels to increase the chances of catching any
|
||||
// obscure bugs
|
||||
for i := 0; i < 10000; i++ {
|
||||
var got string
|
||||
p := recoverPanic(func() {
|
||||
got = DNSLabel(tt.n)
|
||||
})
|
||||
|
||||
if tt.panicErrIs == nil || tt.panicStr == "" {
|
||||
require.Len(t, got, tt.n)
|
||||
asserDNSLabel(t, got)
|
||||
}
|
||||
|
||||
if tt.panicErrIs != nil {
|
||||
require.ErrorIs(t, p.(error), tt.panicErrIs)
|
||||
}
|
||||
|
||||
if tt.panicStr != "" {
|
||||
require.EqualError(t, p.(error), tt.panicStr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := regexp.MustCompile(
|
||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||
)
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
got := UUID()
|
||||
require.Regexp(t, m, got)
|
||||
|
||||
if _, ok := seen[got]; ok {
|
||||
require.FailNow(t, "duplicate UUID")
|
||||
}
|
||||
|
||||
seen[got] = struct{}{}
|
||||
|
||||
raw := strings.ReplaceAll(got, "-", "")
|
||||
b := make([]byte, 16)
|
||||
_, err := hex.Decode(b, []byte(raw))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 4, int(b[6]>>4), "version is not 4")
|
||||
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUIDv7(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := regexp.MustCompile(
|
||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||
)
|
||||
|
||||
// Store timestamps to verify they're increasing
|
||||
var lastTimestampBytes int64
|
||||
var lastUUID string
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
for i := 0; i < 10000; i++ {
|
||||
got := UUIDv7()
|
||||
require.Regexp(t, m, got)
|
||||
|
||||
if _, ok := seen[got]; ok {
|
||||
require.FailNow(t, "duplicate UUID")
|
||||
}
|
||||
|
||||
seen[got] = struct{}{}
|
||||
|
||||
raw := strings.ReplaceAll(got, "-", "")
|
||||
b := make([]byte, 16)
|
||||
_, err := hex.Decode(b, []byte(raw))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check version is 7
|
||||
require.Equal(t, 7, int(b[6]>>4), "version is not 7")
|
||||
|
||||
// Check variant is RFC 4122
|
||||
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||
|
||||
// Extract timestamp bytes
|
||||
timestampBytes := int64(b[0])<<40 | int64(b[1])<<32 | int64(b[2])<<24 |
|
||||
int64(b[3])<<16 | int64(b[4])<<8 | int64(b[5])
|
||||
|
||||
// Verify timestamp is within 10 seconds of current time. This is a
|
||||
// sanity check to ensure the UUID is not too far off from the current
|
||||
// time, while allowing tests to pass on super slow machines.
|
||||
tsTime := time.UnixMilli(timestampBytes)
|
||||
require.WithinDuration(t, time.Now(), tsTime, 10*time.Second,
|
||||
"timestamp is not within 10 seconds of current time",
|
||||
)
|
||||
|
||||
// After the first UUID, verify that UUIDs are monotonically increasing
|
||||
if i > 0 && timestampBytes < lastTimestampBytes {
|
||||
require.FailNow(t, "UUIDs are not monotonically increasing",
|
||||
"current: %s (ts: %d), previous: %s (ts: %d)",
|
||||
got, timestampBytes, lastUUID, lastTimestampBytes)
|
||||
}
|
||||
|
||||
lastTimestampBytes = timestampBytes
|
||||
lastUUID = got
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
var (
|
||||
dnsLabelHeadRx = regexp.MustCompile(`^[a-z]$`)
|
||||
dnsLabelBodyRx = regexp.MustCompile(`^[a-z0-9-]+$`)
|
||||
dnsLabelTailRx = regexp.MustCompile(`^[a-z0-9]$`)
|
||||
)
|
||||
|
||||
func asserDNSLabel(t *testing.T, label string) {
|
||||
require.LessOrEqualf(t, len(label), 63,
|
||||
`DNS label "%s" is longer than 63 characters`, label,
|
||||
)
|
||||
|
||||
require.GreaterOrEqualf(t, len(label), 1,
|
||||
`DNS label "%s" is shorter than 1 character`, label,
|
||||
)
|
||||
|
||||
if len(label) >= 1 {
|
||||
require.Regexpf(t, dnsLabelHeadRx, string(label[0]),
|
||||
`DNS label "%s" must start with a-z`, label,
|
||||
)
|
||||
}
|
||||
if len(label) >= 2 {
|
||||
require.Regexpf(t, dnsLabelTailRx, string(label[len(label)-1]),
|
||||
`DNS label "%s" must end with a-z0-9`, label,
|
||||
)
|
||||
}
|
||||
if len(label) >= 3 {
|
||||
require.Regexpf(t, dnsLabelBodyRx, label[1:len(label)-1],
|
||||
`DNS label "%s" body must only contain a-z0-9-`, label)
|
||||
}
|
||||
if len(label) >= 4 {
|
||||
require.NotEqualf(t, "--", label[2:4],
|
||||
`DNS label "%s" cannot contain "--" as 3rd and 4th char`, label,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func assertAllowedChars(t *testing.T, allowed string, s string) {
|
||||
invalid := ""
|
||||
for _, c := range s {
|
||||
if !strings.Contains(allowed, string(c)) &&
|
||||
!strings.Contains(invalid, string(c)) {
|
||||
invalid += string(c)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Truef(
|
||||
t, len(invalid) == 0, "string contains invalid chars: %s", invalid,
|
||||
)
|
||||
}
|
||||
51
shuffle.go
Normal file
51
shuffle.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package rands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrShuffle = fmt.Errorf("%w: shuffle", Err)
|
||||
ErrInvalidShuffleNegativeN = fmt.Errorf(
|
||||
"%w: n must not be negative", ErrShuffle,
|
||||
)
|
||||
)
|
||||
|
||||
// Shuffle randomizes the order of a collection of n elements using
|
||||
// cryptographically secure random values from crypto/rand. It implements the
|
||||
// Fisher-Yates shuffle algorithm.
|
||||
//
|
||||
// The swap function is called to exchange values at indices i and j. This
|
||||
// signature is compatible with Shuffle from math/rand and math/rand/v2 for easy
|
||||
// migration.
|
||||
func Shuffle(n int, swap func(i, j int)) error {
|
||||
if n < 0 {
|
||||
return ErrInvalidShuffleNegativeN
|
||||
}
|
||||
|
||||
for i := n - 1; i > 0; i-- {
|
||||
j, err := Int(i + 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
swap(i, j)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShuffleSlice randomizes the order of elements in a slice in-place using
|
||||
// cryptographically secure random values from crypto/rand.
|
||||
//
|
||||
// It implements the Fisher-Yates shuffle algorithm.
|
||||
func ShuffleSlice[T any](slice []T) error {
|
||||
// If the slice has one or no elements, there's nothing to shuffle.
|
||||
if len(slice) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Shuffle(len(slice), func(i, j int) {
|
||||
slice[i], slice[j] = slice[j], slice[i]
|
||||
})
|
||||
}
|
||||
32
shuffle_example_test.go
Normal file
32
shuffle_example_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package rands_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/jimeh/rands"
|
||||
)
|
||||
|
||||
func ExampleShuffle() {
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
|
||||
err := rands.Shuffle(len(numbers), func(i, j int) {
|
||||
numbers[i], numbers[j] = numbers[j], numbers[i]
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(numbers) // => [2 4 5 1 3]
|
||||
}
|
||||
|
||||
func ExampleShuffleSlice() {
|
||||
mixed := []any{1, "two", 3.14, true, nil}
|
||||
|
||||
err := rands.ShuffleSlice(mixed)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(mixed) // => [3.14 true 1 two <nil>]
|
||||
}
|
||||
444
shuffle_test.go
Normal file
444
shuffle_test.go
Normal file
@@ -0,0 +1,444 @@
|
||||
package rands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func factorial(n int) int {
|
||||
factorial := 1
|
||||
for i := 1; i <= n; i++ {
|
||||
factorial *= i
|
||||
}
|
||||
|
||||
return factorial
|
||||
}
|
||||
|
||||
func TestShuffle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("n < 0", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := Shuffle(-1, func(_, _ int) {})
|
||||
|
||||
require.ErrorIs(t, err, ErrInvalidShuffleNegativeN)
|
||||
require.ErrorIs(t, err, ErrShuffle)
|
||||
require.ErrorIs(t, err, Err)
|
||||
})
|
||||
|
||||
t.Run("n == 0", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
swapCount := 0
|
||||
err := Shuffle(0, func(_, _ int) {
|
||||
swapCount++
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 0, swapCount)
|
||||
})
|
||||
|
||||
t.Run("n == 1", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
swapCount := 0
|
||||
err := Shuffle(1, func(_, _ int) {
|
||||
swapCount++
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 0, swapCount)
|
||||
})
|
||||
|
||||
t.Run("n == 2", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
swapCount := 0
|
||||
err := Shuffle(2, func(_, _ int) {
|
||||
swapCount++
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 1, swapCount)
|
||||
})
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
arr := make([]int, 100)
|
||||
for i := range arr {
|
||||
arr[i] = i
|
||||
}
|
||||
|
||||
arrCopy := make([]int, len(arr))
|
||||
copy(arrCopy, arr)
|
||||
|
||||
err := Shuffle(len(arr), func(i, j int) {
|
||||
arr[i], arr[j] = arr[j], arr[i]
|
||||
})
|
||||
require.NoError(t, err, "Shuffle returned an error")
|
||||
|
||||
assert.NotEqual(t, arrCopy, arr, "Shuffle did not change the array")
|
||||
assert.ElementsMatch(t, arrCopy, arr, "Shuffle changed elements")
|
||||
})
|
||||
|
||||
t.Run("swaps", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
swapSame := 0
|
||||
swapDifferent := 0
|
||||
arr := make([]int, 100)
|
||||
for j := range arr {
|
||||
arr[j] = j
|
||||
}
|
||||
|
||||
err := Shuffle(len(arr), func(i, j int) {
|
||||
if i == j {
|
||||
swapSame++
|
||||
} else {
|
||||
swapDifferent++
|
||||
}
|
||||
|
||||
arr[i], arr[j] = arr[j], arr[i]
|
||||
})
|
||||
require.NoError(t, err, "Shuffle returned an error")
|
||||
|
||||
// Fisher-Yates with n elements should make exactly n-1 swaps
|
||||
assert.Equal(t, len(arr)-1, swapSame+swapDifferent,
|
||||
"Unexpected swaps count",
|
||||
)
|
||||
|
||||
// Ensure we have more different-element swaps than self-swaps. The
|
||||
// lower the input shuffle n value, the more likely this assertion will
|
||||
// fail. For a n=100 shuffle, this is exceptionally unlikely to fail.
|
||||
assert.Greater(t, swapDifferent, swapSame,
|
||||
"Expected more different-element swaps than self-swaps",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("swap ranges", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
n := 32
|
||||
runs := 1000
|
||||
|
||||
for run := 0; run < runs; run++ {
|
||||
called := 0
|
||||
err := Shuffle(n, func(i, j int) {
|
||||
called++
|
||||
|
||||
// Verify indices are in bounds.
|
||||
assert.True(t, i >= 0 && i < n, "Out of bounds index i = %d", i)
|
||||
assert.True(t, j >= 0 && j < n, "Out of bounds index j = %d", j)
|
||||
|
||||
// For Fisher-Yates, i should be > 0 and j should be in range
|
||||
// [0,i].
|
||||
assert.Greater(t, i, 0, "Expected i > 0, got i=%d", i)
|
||||
assert.True(t,
|
||||
j >= 0 && j <= i,
|
||||
"Expected j in range [0,%d], got j=%d", i, j,
|
||||
)
|
||||
})
|
||||
require.NoError(t, err, "Shuffle returned an error")
|
||||
|
||||
// Fisher-Yates with n elements should make exactly n-1 swaps
|
||||
expected := n - 1
|
||||
assert.Equal(t, expected, called,
|
||||
"Expected %d swap calls, got %d", expected, called,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("all permutations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Use a small array of 5 elements to make it feasible to track all
|
||||
// permutations.
|
||||
n := 5
|
||||
fact := factorial(n) // 120
|
||||
runs := fact * 3000 // 360000
|
||||
|
||||
permCounts := make(map[string]int)
|
||||
for i := 0; i < runs; i++ {
|
||||
arr := make([]int, n)
|
||||
for i := range arr {
|
||||
arr[i] = i
|
||||
}
|
||||
|
||||
err := Shuffle(len(arr), func(i, j int) {
|
||||
arr[i], arr[j] = arr[j], arr[i]
|
||||
})
|
||||
require.NoError(t, err, "Shuffle returned an error")
|
||||
|
||||
// Convert the permutation to a string key and count it.
|
||||
key := fmt.Sprintf("%v", arr)
|
||||
permCounts[key]++
|
||||
}
|
||||
|
||||
assert.Equal(t, fact, len(permCounts),
|
||||
"Expected %d different permutations", fact,
|
||||
)
|
||||
|
||||
wantCount := float64(runs) / float64(fact)
|
||||
margin := 0.15
|
||||
minAcceptable := int(wantCount * (1 - margin))
|
||||
maxAcceptable := int(wantCount * (1 + margin))
|
||||
|
||||
for perm, count := range permCounts {
|
||||
assert.True(t,
|
||||
count >= minAcceptable && count <= maxAcceptable,
|
||||
"Non-uniform distribution for %s: count=%d, expected=%v±%v",
|
||||
perm, count, wantCount, wantCount*margin,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("distribution", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Track which positions received which random indices
|
||||
n := 100
|
||||
posCounts := make([]map[int]int, n)
|
||||
for i := range posCounts {
|
||||
posCounts[i] = make(map[int]int)
|
||||
}
|
||||
|
||||
runs := 3000
|
||||
for run := 0; run < runs; run++ {
|
||||
err := Shuffle(n, func(i, j int) {
|
||||
posCounts[i][j]++
|
||||
})
|
||||
require.NoError(t, err, "Shuffle returned an error")
|
||||
}
|
||||
|
||||
// For each position, check that it received a reasonable distribution.
|
||||
for i := n - 1; i >= n-len(posCounts); i-- {
|
||||
// Calculate how many unique positions we should expect.
|
||||
// Position i should receive random positions from 0 to i, and
|
||||
// allow for some statistical variation.
|
||||
want := int(float64(i+1) * 0.9)
|
||||
assert.GreaterOrEqual(t,
|
||||
len(posCounts[i]), want,
|
||||
"Position %d: expected ~%d unique indices, got %d",
|
||||
i, want, len(posCounts[i]),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkShuffle(b *testing.B) {
|
||||
ranges := []int{32, 64, 128, 1024, 4096}
|
||||
for _, n := range ranges {
|
||||
b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Shuffle(n, func(_, _ int) {})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShuffleSlice(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("empty slice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slice := []int{}
|
||||
err := ShuffleSlice(slice)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, slice)
|
||||
})
|
||||
|
||||
t.Run("single element", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slice := []int{42}
|
||||
origSlice := make([]int, len(slice))
|
||||
copy(origSlice, slice)
|
||||
|
||||
err := ShuffleSlice(slice)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t,
|
||||
origSlice, slice, "Single element slice should remain unchanged",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("two elements", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slice := []int{1, 2}
|
||||
origSlice := make([]int, len(slice))
|
||||
copy(origSlice, slice)
|
||||
|
||||
err := ShuffleSlice(slice)
|
||||
require.NoError(t, err)
|
||||
|
||||
// With two elements, the slice might remain the same or be swapped
|
||||
assert.Len(t, slice, len(origSlice))
|
||||
assert.ElementsMatch(t, origSlice, slice)
|
||||
})
|
||||
|
||||
t.Run("basic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
slice := make([]int, 100)
|
||||
for i := range slice {
|
||||
slice[i] = i
|
||||
}
|
||||
|
||||
sliceCopy := make([]int, len(slice))
|
||||
copy(sliceCopy, slice)
|
||||
|
||||
err := ShuffleSlice(slice)
|
||||
require.NoError(t, err, "ShuffleSlice returned an error")
|
||||
|
||||
assert.NotEqual(t,
|
||||
sliceCopy, slice, "ShuffleSlice did not change the slice",
|
||||
)
|
||||
assert.ElementsMatch(t,
|
||||
sliceCopy, slice, "ShuffleSlice changed elements",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("string slice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
strSlice := []string{"a", "b", "c", "d", "e"}
|
||||
strCopy := make([]string, len(strSlice))
|
||||
copy(strCopy, strSlice)
|
||||
|
||||
err := ShuffleSlice(strSlice)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.ElementsMatch(t, strCopy, strSlice)
|
||||
})
|
||||
|
||||
t.Run("struct slice", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type testStruct struct {
|
||||
id int
|
||||
name string
|
||||
}
|
||||
structSlice := []testStruct{
|
||||
{1, "one"},
|
||||
{2, "two"},
|
||||
{3, "three"},
|
||||
{4, "four"},
|
||||
}
|
||||
structCopy := make([]testStruct, len(structSlice))
|
||||
copy(structCopy, structSlice)
|
||||
|
||||
err := ShuffleSlice(structSlice)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, structCopy, structSlice)
|
||||
})
|
||||
|
||||
t.Run("all permutations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Use a small slice of 5 elements to make it feasible to track all
|
||||
// permutations.
|
||||
n := 5
|
||||
fact := factorial(n) // 120
|
||||
runs := fact * 3000 // 360000
|
||||
|
||||
permCounts := make(map[string]int)
|
||||
for i := 0; i < runs; i++ {
|
||||
slice := make([]int, n)
|
||||
for j := range slice {
|
||||
slice[j] = j
|
||||
}
|
||||
|
||||
err := ShuffleSlice(slice)
|
||||
require.NoError(t, err, "ShuffleSlice returned an error")
|
||||
|
||||
// Convert the permutation to a string key and count it.
|
||||
key := fmt.Sprintf("%v", slice)
|
||||
permCounts[key]++
|
||||
}
|
||||
|
||||
assert.Equal(t, fact, len(permCounts),
|
||||
"Expected %d different permutations", fact,
|
||||
)
|
||||
|
||||
wantCount := float64(runs) / float64(fact)
|
||||
margin := 0.15
|
||||
minAcceptable := int(wantCount * (1 - margin))
|
||||
maxAcceptable := int(wantCount * (1 + margin))
|
||||
|
||||
for perm, count := range permCounts {
|
||||
assert.True(t,
|
||||
count >= minAcceptable && count <= maxAcceptable,
|
||||
"Non-uniform distribution for %s: count=%d, expected=%v±%v",
|
||||
perm, count, wantCount, wantCount*margin,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("distribution", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Track where each original index ends up after shuffling
|
||||
n := 100
|
||||
// posCounts[originalPos][newPos] tracks how many times
|
||||
// the element originally at position i ended up at position j
|
||||
posCounts := make([]map[int]int, n)
|
||||
for i := range posCounts {
|
||||
posCounts[i] = make(map[int]int)
|
||||
}
|
||||
|
||||
runs := 3000
|
||||
for run := 0; run < runs; run++ {
|
||||
// Create a slice where the value is its original position
|
||||
slice := make([]int, n)
|
||||
for i := range slice {
|
||||
slice[i] = i
|
||||
}
|
||||
|
||||
err := ShuffleSlice(slice)
|
||||
require.NoError(t, err, "ShuffleSlice returned an error")
|
||||
|
||||
// Track where each original position ended up
|
||||
for newPos, origPos := range slice {
|
||||
posCounts[origPos][newPos]++
|
||||
}
|
||||
}
|
||||
|
||||
// For each original position, check that it was distributed
|
||||
// reasonably across all possible new positions
|
||||
for i := n - 1; i >= n-len(posCounts); i-- {
|
||||
// Calculate how many unique positions we should expect.
|
||||
// Position i should receive random positions from 0 to i, and
|
||||
// allow for some statistical variation.
|
||||
want := int(float64(i+1) * 0.9)
|
||||
assert.GreaterOrEqual(t,
|
||||
len(posCounts[i]), want,
|
||||
"Original position %d: expected ~%d unique positions, got %d",
|
||||
i, want, len(posCounts[i]),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkShuffleSlice(b *testing.B) {
|
||||
ranges := []int{32, 64, 128, 1024, 4096}
|
||||
for _, n := range ranges {
|
||||
b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
|
||||
b.StopTimer()
|
||||
slice := make([]int, n)
|
||||
for i := range slice {
|
||||
slice[i] = i
|
||||
}
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = ShuffleSlice(slice)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
64
strings.go
64
strings.go
@@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"unicode"
|
||||
|
||||
"github.com/jimeh/rands/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,12 +23,12 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
errNonASCIIAlphabet = fmt.Errorf(
|
||||
"%w: alphabet contains non-ASCII characters", errBase,
|
||||
ErrNonASCIIAlphabet = fmt.Errorf(
|
||||
"%w: alphabet contains non-ASCII characters", Err,
|
||||
)
|
||||
|
||||
errDNSLabelLength = fmt.Errorf(
|
||||
"%w: DNS labels must be between 1 and 63 characters in length", errBase,
|
||||
ErrDNSLabelLength = fmt.Errorf(
|
||||
"%w: DNS labels must be between 1 and 63 characters in length", Err,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -126,7 +128,7 @@ func LowerNumeric(n int) (string, error) {
|
||||
// UnicodeString() if you need a alphabet with Unicode characters.
|
||||
func String(n int, alphabet string) (string, error) {
|
||||
if !isASCII(alphabet) {
|
||||
return "", errNonASCIIAlphabet
|
||||
return "", ErrNonASCIIAlphabet
|
||||
}
|
||||
|
||||
l := big.NewInt(int64(len(alphabet)))
|
||||
@@ -164,24 +166,22 @@ func UnicodeString(n int, alphabet []rune) (string, error) {
|
||||
}
|
||||
|
||||
// DNSLabel returns a random string of n length in a DNS label compliant format
|
||||
// as defined in RFC 1035, section 2.3.1:
|
||||
// https://www.rfc-editor.org/rfc/rfc1035.html#section-2.3.1
|
||||
// as defined in RFC 1035, section 2.3.1.
|
||||
//
|
||||
// It also adheres to RFC 5891, section 4.2.3.1:
|
||||
// https://www.rfc-editor.org/rfc/rfc5891.html#section-4.2.3.1
|
||||
// It also adheres to RFC 5891, section 4.2.3.1.
|
||||
//
|
||||
// In summary, the generated random string will:
|
||||
//
|
||||
// - be between 1 and 63 characters in length, other n values returns a error
|
||||
// - first character will be one of a-z
|
||||
// - last character will be one of a-z or 0-9
|
||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||
// - be between 1 and 63 characters in length, other n values returns a error
|
||||
// - first character will be one of a-z
|
||||
// - last character will be one of a-z or 0-9
|
||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||
func DNSLabel(n int) (string, error) {
|
||||
switch {
|
||||
case n < 1 || n > 63:
|
||||
return "", errDNSLabelLength
|
||||
return "", ErrDNSLabelLength
|
||||
case n == 1:
|
||||
return String(1, lowerChars)
|
||||
default:
|
||||
@@ -235,6 +235,38 @@ func DNSLabel(n int) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// UUID returns a random UUID v4 in string format as defined by RFC 4122,
|
||||
// section 4.4.
|
||||
func UUID() (string, error) {
|
||||
uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return uuid.String(), nil
|
||||
}
|
||||
|
||||
// UUIDv7 returns a time-ordered UUID v7 in string format.
|
||||
//
|
||||
// The UUID v7 format uses a timestamp with millisecond precision in the most
|
||||
// significant bits, followed by random data. This provides both uniqueness and
|
||||
// chronological ordering, making it ideal for database primary keys and
|
||||
// situations where sorting by creation time is desired.
|
||||
//
|
||||
// References:
|
||||
// - https://uuid7.com/
|
||||
// - https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
|
||||
//
|
||||
//nolint:lll
|
||||
func UUIDv7() (string, error) {
|
||||
uuid, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return uuid.String(), nil
|
||||
}
|
||||
|
||||
func isASCII(s string) bool {
|
||||
for _, c := range s {
|
||||
if c > unicode.MaxASCII {
|
||||
|
||||
@@ -2,71 +2,142 @@ package rands_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/jimeh/rands"
|
||||
)
|
||||
|
||||
func ExampleBase64() {
|
||||
s, _ := rands.Base64(16)
|
||||
fmt.Println(s) // => CYxqEdUB1Rzno3SyZu2g/g==
|
||||
s, err := rands.Base64(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => nYQLhIYTqh8oH/W4hZuXMQ==
|
||||
}
|
||||
|
||||
func ExampleBase64URL() {
|
||||
s, _ := rands.Base64URL(16)
|
||||
fmt.Println(s) // => zlqw9aFqcFggbk2asn3_aQ
|
||||
s, err := rands.Base64URL(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => zI_zrc1l0uPT4MxncR6e5w
|
||||
}
|
||||
|
||||
func ExampleHex() {
|
||||
s, _ := rands.Hex(16)
|
||||
fmt.Println(s) // => 956e2ec9e7f19ddd58bb935826926531
|
||||
s, err := rands.Hex(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => b59e8977a13f3c030bd2ea1002ec8081
|
||||
}
|
||||
|
||||
func ExampleAlphanumeric() {
|
||||
s, _ := rands.Alphanumeric(16)
|
||||
fmt.Println(s) // => Fvk1PkrmG5crgOjT
|
||||
s, err := rands.Alphanumeric(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => EgPieCBO7MuWhHtj
|
||||
}
|
||||
|
||||
func ExampleAlphabetic() {
|
||||
s, _ := rands.Alphabetic(16)
|
||||
fmt.Println(s) // => XEJIzcZufHkuUmRM
|
||||
s, err := rands.Alphabetic(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => VzcovEqvMRBWUtQC
|
||||
}
|
||||
|
||||
func ExampleUpper() {
|
||||
s, _ := rands.Upper(16)
|
||||
fmt.Println(s) // => UMAGAFPPNDRGLUPZ
|
||||
s, err := rands.Upper(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => MCZEGPWGYKNUEDCK
|
||||
}
|
||||
|
||||
func ExampleUpperNumeric() {
|
||||
s, _ := rands.UpperNumeric(16)
|
||||
fmt.Println(s) // => DF0CQS0TK9CPUO3E
|
||||
s, err := rands.UpperNumeric(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => 6LLPBBUW77B26X2X
|
||||
}
|
||||
|
||||
func ExampleLower() {
|
||||
s, _ := rands.Lower(16)
|
||||
fmt.Println(s) // => ocsmggykzrxzfwgt
|
||||
s, err := rands.Lower(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => dhoqhrqljadsztaa
|
||||
}
|
||||
|
||||
func ExampleLowerNumeric() {
|
||||
s, _ := rands.LowerNumeric(16)
|
||||
fmt.Println(s) // => rwlv7a1p7klqffs5
|
||||
s, err := rands.LowerNumeric(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => th1z1b1d24l5h8pu
|
||||
}
|
||||
|
||||
func ExampleNumeric() {
|
||||
s, _ := rands.Numeric(16)
|
||||
fmt.Println(s) // => 9403373143598295
|
||||
s, err := rands.Numeric(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => 3378802228987741
|
||||
}
|
||||
|
||||
func ExampleString() {
|
||||
s, _ := rands.String(16, "abcdefABCDEF")
|
||||
fmt.Println(s) // => adCDCaDEdeffeDeb
|
||||
s, err := rands.String(16, "abcdefABCDEF")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => BAFffADaadeeacfa
|
||||
}
|
||||
|
||||
func ExampleUnicodeString() {
|
||||
s, _ := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大"))
|
||||
fmt.Println(s) // => 下下口九力下土夕下土八上二夕大三
|
||||
s, err := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => 八三口上土土七入力夕人力下三上力
|
||||
}
|
||||
|
||||
func ExampleDNSLabel() {
|
||||
s, _ := rands.DNSLabel(16)
|
||||
fmt.Println(s) // => z0ij9o8qkbs0ru-h
|
||||
s, err := rands.DNSLabel(16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => ab-sbh5q0gfb6sqo
|
||||
}
|
||||
|
||||
func ExampleUUID() {
|
||||
s, err := rands.UUID()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => 6a1c4f65-d5d6-4a28-aa51-eaa94fa7ad4a
|
||||
}
|
||||
|
||||
func ExampleUUIDv7() {
|
||||
s, err := rands.UUIDv7()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println(s) // => 01954a3a-a06f-7848-b836-bced92ae5a1a
|
||||
}
|
||||
|
||||
244
strings_test.go
244
strings_test.go
@@ -2,16 +2,19 @@ package rands
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"encoding/hex"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHex(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "0123456789abcdef"
|
||||
|
||||
for _, tt := range testCases {
|
||||
@@ -35,6 +38,8 @@ func BenchmarkHex(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestBase64(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
|
||||
"0123456789+/="
|
||||
|
||||
@@ -62,6 +67,8 @@ func BenchmarkBase64(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestBase64URL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
|
||||
"0123456789-_"
|
||||
|
||||
@@ -89,6 +96,8 @@ func BenchmarkBase64URL(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestAlphanumeric(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
for _, tt := range testCases {
|
||||
@@ -112,6 +121,8 @@ func BenchmarkAlphanumeric(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestAlphabetic(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
for _, tt := range testCases {
|
||||
@@ -135,6 +146,8 @@ func BenchmarkAlphabetic(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestNumeric(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "0123456789"
|
||||
|
||||
for _, tt := range testCases {
|
||||
@@ -158,6 +171,8 @@ func BenchmarkNumeric(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestUpper(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
for _, tt := range testCases {
|
||||
@@ -181,6 +196,8 @@ func BenchmarkUpper(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestUpperNumeric(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
for _, tt := range testCases {
|
||||
@@ -204,6 +221,8 @@ func BenchmarkUpperNumeric(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestLower(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
for _, tt := range testCases {
|
||||
@@ -227,6 +246,8 @@ func BenchmarkLower(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestLowerNumeric(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
allowed := "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
for _, tt := range testCases {
|
||||
@@ -261,7 +282,7 @@ var stringTestCases = []struct {
|
||||
n: 32,
|
||||
alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" +
|
||||
"άίόύώέϊϋΐΰΆΈΌΏΎΊ",
|
||||
errIs: errNonASCIIAlphabet,
|
||||
errIs: ErrNonASCIIAlphabet,
|
||||
errStr: "rands: alphabet contains non-ASCII characters",
|
||||
},
|
||||
{
|
||||
@@ -269,7 +290,7 @@ var stringTestCases = []struct {
|
||||
n: 32,
|
||||
alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" +
|
||||
"时道出而要于就下得可你年生",
|
||||
errIs: errNonASCIIAlphabet,
|
||||
errIs: ErrNonASCIIAlphabet,
|
||||
errStr: "rands: alphabet contains non-ASCII characters",
|
||||
},
|
||||
{
|
||||
@@ -277,7 +298,7 @@ var stringTestCases = []struct {
|
||||
n: 32,
|
||||
alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" +
|
||||
"手文日月木水火犬王正出本右四",
|
||||
errIs: errNonASCIIAlphabet,
|
||||
errIs: ErrNonASCIIAlphabet,
|
||||
errStr: "rands: alphabet contains non-ASCII characters",
|
||||
},
|
||||
{
|
||||
@@ -348,6 +369,8 @@ var stringTestCases = []struct {
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range stringTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := String(tt.n, tt.alphabet)
|
||||
@@ -358,7 +381,7 @@ func TestString(t *testing.T) {
|
||||
}
|
||||
|
||||
if tt.errIs != nil {
|
||||
assert.True(t, errors.Is(err, errNonASCIIAlphabet))
|
||||
assert.ErrorIs(t, err, tt.errIs)
|
||||
}
|
||||
|
||||
if tt.errStr != "" {
|
||||
@@ -475,6 +498,8 @@ var unicodeStringTestCases = []struct {
|
||||
}
|
||||
|
||||
func TestUnicodeString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range unicodeStringTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, _ := UnicodeString(tt.n, []rune(tt.alphabet))
|
||||
@@ -497,54 +522,57 @@ func BenchmarkUnicodeString(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
var dnsLabelTestCases = []struct {
|
||||
name string
|
||||
n int
|
||||
errIs error
|
||||
errStr string
|
||||
}{
|
||||
{
|
||||
name: "n=-128",
|
||||
n: -128,
|
||||
errIs: ErrDNSLabelLength,
|
||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
{
|
||||
name: "n=0",
|
||||
n: 0,
|
||||
errIs: ErrDNSLabelLength,
|
||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
{name: "n=1", n: 1},
|
||||
{name: "n=2", n: 2},
|
||||
{name: "n=3", n: 3},
|
||||
{name: "n=4", n: 4},
|
||||
{name: "n=5", n: 5},
|
||||
{name: "n=6", n: 6},
|
||||
{name: "n=7", n: 7},
|
||||
{name: "n=8", n: 8},
|
||||
{name: "n=16", n: 16},
|
||||
{name: "n=32", n: 32},
|
||||
{name: "n=63", n: 63},
|
||||
{
|
||||
name: "n=64",
|
||||
n: 64,
|
||||
errIs: ErrDNSLabelLength,
|
||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
{
|
||||
name: "n=128",
|
||||
n: 128,
|
||||
errIs: ErrDNSLabelLength,
|
||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
}
|
||||
|
||||
func TestDNSLabel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
n int
|
||||
errIs error
|
||||
errStr string
|
||||
}{
|
||||
{
|
||||
name: "n=-128",
|
||||
n: -128,
|
||||
errIs: errDNSLabelLength,
|
||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
{
|
||||
name: "n=0",
|
||||
n: 0,
|
||||
errIs: errDNSLabelLength,
|
||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
{name: "n=1", n: 1},
|
||||
{name: "n=2", n: 2},
|
||||
{name: "n=3", n: 3},
|
||||
{name: "n=4", n: 4},
|
||||
{name: "n=5", n: 5},
|
||||
{name: "n=6", n: 6},
|
||||
{name: "n=7", n: 7},
|
||||
{name: "n=8", n: 8},
|
||||
{name: "n=16", n: 16},
|
||||
{name: "n=32", n: 32},
|
||||
{name: "n=63", n: 63},
|
||||
{
|
||||
name: "n=64",
|
||||
n: 64,
|
||||
errIs: errDNSLabelLength,
|
||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
{
|
||||
name: "n=128",
|
||||
n: 128,
|
||||
errIs: errDNSLabelLength,
|
||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||
"in length",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Parallel()
|
||||
|
||||
for _, tt := range dnsLabelTestCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// generate lots of labels to increase the chances of catching any
|
||||
// obscure bugs
|
||||
@@ -557,7 +585,7 @@ func TestDNSLabel(t *testing.T) {
|
||||
}
|
||||
|
||||
if tt.errIs != nil {
|
||||
require.True(t, errors.Is(err, errDNSLabelLength))
|
||||
require.ErrorIs(t, err, tt.errIs)
|
||||
}
|
||||
|
||||
if tt.errStr != "" {
|
||||
@@ -568,6 +596,116 @@ func TestDNSLabel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDNSLabel(b *testing.B) {
|
||||
for _, tt := range dnsLabelTestCases {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = DNSLabel(tt.n)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := regexp.MustCompile(
|
||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||
)
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
got, err := UUID()
|
||||
require.NoError(t, err)
|
||||
require.Regexp(t, m, got)
|
||||
|
||||
if _, ok := seen[got]; ok {
|
||||
require.FailNow(t, "duplicate UUID")
|
||||
}
|
||||
|
||||
seen[got] = struct{}{}
|
||||
|
||||
raw := strings.ReplaceAll(got, "-", "")
|
||||
b := make([]byte, 16)
|
||||
_, err = hex.Decode(b, []byte(raw))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 4, int(b[6]>>4), "version is not 4")
|
||||
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = UUID()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUIDv7(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := regexp.MustCompile(
|
||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||
)
|
||||
|
||||
// Store timestamps to verify they're increasing
|
||||
var lastTimestampBytes int64
|
||||
var lastUUID string
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
for i := 0; i < 10000; i++ {
|
||||
got, err := UUIDv7()
|
||||
require.NoError(t, err)
|
||||
require.Regexp(t, m, got)
|
||||
|
||||
if _, ok := seen[got]; ok {
|
||||
require.FailNow(t, "duplicate UUID")
|
||||
}
|
||||
|
||||
seen[got] = struct{}{}
|
||||
|
||||
raw := strings.ReplaceAll(got, "-", "")
|
||||
b := make([]byte, 16)
|
||||
_, err = hex.Decode(b, []byte(raw))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check version is 7
|
||||
require.Equal(t, 7, int(b[6]>>4), "version is not 7")
|
||||
|
||||
// Check variant is RFC 4122
|
||||
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||
|
||||
// Extract timestamp bytes
|
||||
timestampBytes := int64(b[0])<<40 | int64(b[1])<<32 |
|
||||
int64(b[2])<<24 | int64(b[3])<<16 | int64(b[4])<<8 | int64(b[5])
|
||||
|
||||
// Verify timestamp is within 10 seconds of current time. This is a
|
||||
// sanity check to ensure the UUID is not too far off from the current
|
||||
// time, while allowing tests to pass on super slow machines.
|
||||
tsTime := time.UnixMilli(timestampBytes)
|
||||
require.WithinDuration(t, time.Now(), tsTime, 10*time.Second,
|
||||
"timestamp is not within 10 seconds of current time",
|
||||
)
|
||||
|
||||
// After the first UUID, verify that UUIDs are monotonically increasing
|
||||
if i > 0 && timestampBytes < lastTimestampBytes {
|
||||
require.FailNow(t, "UUIDs are not monotonically increasing",
|
||||
"current: %s (ts: %d), previous: %s (ts: %d)",
|
||||
got, timestampBytes, lastUUID, lastTimestampBytes)
|
||||
}
|
||||
|
||||
lastTimestampBytes = timestampBytes
|
||||
lastUUID = got
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUIDv7(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = UUIDv7()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
24
uuid/random.go
Normal file
24
uuid/random.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
)
|
||||
|
||||
// NewRandom returns a random UUID v4 in string format as defined by RFC 4122,
|
||||
// section 4.4.
|
||||
func NewRandom() (UUID, error) {
|
||||
var u UUID
|
||||
|
||||
// Fill the entire UUID with random bytes.
|
||||
_, err := rand.Read(u[:])
|
||||
if err != nil {
|
||||
// This should never happen.
|
||||
return u, err
|
||||
}
|
||||
|
||||
// Set the version and variant bits.
|
||||
u[6] = (u[6] & 0x0f) | 0x40 // Version: 4 (random)
|
||||
u[8] = (u[8] & 0x3f) | 0x80 // Variant: RFC 4122
|
||||
|
||||
return u, nil
|
||||
}
|
||||
39
uuid/random_test.go
Normal file
39
uuid/random_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewRandom(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := regexp.MustCompile(
|
||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||
)
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
got, err := NewRandom()
|
||||
require.NoError(t, err)
|
||||
require.Regexp(t, m, got.String())
|
||||
|
||||
if _, ok := seen[got.String()]; ok {
|
||||
require.FailNow(t, "duplicate UUID")
|
||||
}
|
||||
|
||||
seen[got.String()] = struct{}{}
|
||||
|
||||
require.Equal(t, 4, int(got[6]>>4), "version is not 4")
|
||||
require.Equal(t, byte(0x80), got[8]&0xc0, "variant is not RFC 4122")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewRandom(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = NewRandom()
|
||||
}
|
||||
}
|
||||
101
uuid/uuid.go
Normal file
101
uuid/uuid.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Package uuid provides a UUID type and associated utilities.
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
Err = errors.New("uuid")
|
||||
ErrInvalidLength = fmt.Errorf("%w: invalid length", Err)
|
||||
)
|
||||
|
||||
const (
|
||||
hyphen = '-'
|
||||
)
|
||||
|
||||
// UUID represents a Universally Unique Identifier (UUID).
|
||||
// It is implemented as a 16-byte array.
|
||||
type UUID [16]byte
|
||||
|
||||
// String returns the string representation of the UUID,
|
||||
// formatted according to RFC 4122 (8-4-4-4-12 hex digits separated by hyphens).
|
||||
func (u UUID) String() string {
|
||||
dst := make([]byte, 36)
|
||||
|
||||
hex.Encode(dst[0:8], u[0:4])
|
||||
dst[8] = hyphen
|
||||
hex.Encode(dst[9:13], u[4:6])
|
||||
dst[13] = hyphen
|
||||
hex.Encode(dst[14:18], u[6:8])
|
||||
dst[18] = hyphen
|
||||
hex.Encode(dst[19:23], u[8:10])
|
||||
dst[23] = hyphen
|
||||
hex.Encode(dst[24:], u[10:])
|
||||
|
||||
return string(dst)
|
||||
}
|
||||
|
||||
// FromBytes creates a UUID from a byte slice.
|
||||
//
|
||||
// If the slice isn't exactly 16 bytes, it returns an empty UUID.
|
||||
func FromBytes(b []byte) (UUID, error) {
|
||||
var u UUID
|
||||
if len(b) != 16 {
|
||||
return u, ErrInvalidLength
|
||||
}
|
||||
|
||||
copy(u[:], b)
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// FromString creates a UUID from a string.
|
||||
//
|
||||
// If the string isn't exactly 36 characters, it returns an empty UUID.
|
||||
func FromString(s string) (UUID, error) {
|
||||
if len(s) != 36 {
|
||||
return UUID{}, ErrInvalidLength
|
||||
}
|
||||
|
||||
raw := strings.ReplaceAll(s, "-", "")
|
||||
|
||||
u := UUID{}
|
||||
_, err := hex.Decode(u[:], []byte(raw))
|
||||
if err != nil {
|
||||
return UUID{}, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// Time returns the timestamp of the UUID if it's a version 7 (time-ordered)
|
||||
// UUID. Otherwise, it returns the zero time.
|
||||
func (u UUID) Time() (t time.Time, ok bool) {
|
||||
if u.Version() != 7 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
// Extract the timestamp from the UUID.
|
||||
// For UUIDv7, only the first 6 bytes contain the timestamp in milliseconds
|
||||
timestamp := uint64(u[0])<<40 | uint64(u[1])<<32 | uint64(u[2])<<24 |
|
||||
uint64(u[3])<<16 | uint64(u[4])<<8 | uint64(u[5])
|
||||
|
||||
if timestamp > math.MaxInt64 {
|
||||
// This shouldn't happen until year 292,272,993.
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
return time.UnixMilli(int64(timestamp)), true
|
||||
}
|
||||
|
||||
// Version returns the version of the UUID.
|
||||
func (u UUID) Version() int {
|
||||
// The version is stored in the 4 most significant bits of byte 6
|
||||
return int(u[6] >> 4)
|
||||
}
|
||||
475
uuid/uuid_test.go
Normal file
475
uuid/uuid_test.go
Normal file
@@ -0,0 +1,475 @@
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUUID_String(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
uuid UUID
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Zero UUID",
|
||||
uuid: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
expected: "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
{
|
||||
name: "Random UUID",
|
||||
uuid: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0xde, 0xf0,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
expected: "12345678-9abc-def0-1234-56789abcdef0",
|
||||
},
|
||||
{
|
||||
name: "UUID with max values",
|
||||
uuid: UUID{
|
||||
0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff,
|
||||
0xff, 0xff,
|
||||
0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
},
|
||||
expected: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.uuid.String()
|
||||
assert.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID_String(b *testing.B) {
|
||||
u := UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0xde, 0xf0,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = u.String()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
bytes []byte
|
||||
want UUID
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "Valid 16 bytes",
|
||||
bytes: []byte{
|
||||
0x12,
|
||||
0x34,
|
||||
0x56,
|
||||
0x78,
|
||||
0x9a,
|
||||
0xbc,
|
||||
0xde,
|
||||
0xf0,
|
||||
0x12,
|
||||
0x34,
|
||||
0x56,
|
||||
0x78,
|
||||
0x9a,
|
||||
0xbc,
|
||||
0xde,
|
||||
0xf0,
|
||||
},
|
||||
want: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0xde, 0xf0,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty bytes",
|
||||
bytes: []byte{},
|
||||
want: UUID{},
|
||||
wantErr: ErrInvalidLength,
|
||||
},
|
||||
{
|
||||
name: "Too few bytes",
|
||||
bytes: []byte{0x12, 0x34, 0x56, 0x78, 0x9a},
|
||||
want: UUID{},
|
||||
wantErr: ErrInvalidLength,
|
||||
},
|
||||
{
|
||||
name: "Too many bytes",
|
||||
bytes: []byte{
|
||||
0x12,
|
||||
0x34,
|
||||
0x56,
|
||||
0x78,
|
||||
0x9a,
|
||||
0xbc,
|
||||
0xde,
|
||||
0xf0,
|
||||
0x12,
|
||||
0x34,
|
||||
0x56,
|
||||
0x78,
|
||||
0x9a,
|
||||
0xbc,
|
||||
0xde,
|
||||
0xf0,
|
||||
0xab,
|
||||
},
|
||||
want: UUID{},
|
||||
wantErr: ErrInvalidLength,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := FromBytes(tt.bytes)
|
||||
|
||||
if tt.wantErr != nil {
|
||||
assert.EqualError(t, err, tt.wantErr.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromBytes(b *testing.B) {
|
||||
bytes := []byte{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0xde, 0xf0,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = FromBytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
str string
|
||||
want UUID
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "Valid UUID string",
|
||||
str: "12345678-9abc-def0-1234-56789abcdef0",
|
||||
want: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0xde, 0xf0,
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty string",
|
||||
str: "",
|
||||
want: UUID{},
|
||||
wantErr: ErrInvalidLength,
|
||||
},
|
||||
{
|
||||
name: "Too short string",
|
||||
str: "12345678-9abc-def0-1234-56789abcde",
|
||||
want: UUID{},
|
||||
wantErr: ErrInvalidLength,
|
||||
},
|
||||
{
|
||||
name: "Too long string",
|
||||
str: "12345678-9abc-def0-1234-56789abcdef0a",
|
||||
want: UUID{},
|
||||
wantErr: ErrInvalidLength,
|
||||
},
|
||||
{
|
||||
name: "Invalid characters",
|
||||
str: "12345678-9abc-defg-1234-56789abcdef0",
|
||||
want: UUID{},
|
||||
wantErr: errors.New("encoding/hex: invalid byte: U+0067 'g'"),
|
||||
},
|
||||
{
|
||||
name: "Zero UUID",
|
||||
str: "00000000-0000-0000-0000-000000000000",
|
||||
want: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Max value UUID",
|
||||
str: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||
want: UUID{
|
||||
0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff,
|
||||
0xff, 0xff,
|
||||
0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := FromString(tt.str)
|
||||
|
||||
if tt.wantErr != nil {
|
||||
assert.EqualError(t, err, tt.wantErr.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFromString(b *testing.B) {
|
||||
uuidStr := "12345678-9abc-def0-1234-56789abcdef0"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = FromString(uuidStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUID_Time(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Define a reference time for testing.
|
||||
refTime := time.Date(2023, 5, 15, 12, 0, 0, 0, time.UTC)
|
||||
// The timestamp in milliseconds (Unix timestamp * 1000 in 6 bytes).
|
||||
timestampMillis := refTime.UnixMilli()
|
||||
|
||||
// Create bytes for the timestamp (first 6 bytes of UUID).
|
||||
timestampBytes := []byte{
|
||||
byte(timestampMillis >> 40),
|
||||
byte(timestampMillis >> 32),
|
||||
byte(timestampMillis >> 24),
|
||||
byte(timestampMillis >> 16),
|
||||
byte(timestampMillis >> 8),
|
||||
byte(timestampMillis),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
uuid UUID
|
||||
wantTime time.Time
|
||||
wantOk bool
|
||||
}{
|
||||
{
|
||||
name: "Version 7 UUID",
|
||||
uuid: func() UUID {
|
||||
var u UUID
|
||||
// Set first 6 bytes to timestamp.
|
||||
copy(u[:6], timestampBytes)
|
||||
// Set version to 7 (0111 as the high nibble of byte 6).
|
||||
u[6] = (u[6] & 0x0F) | 0x70
|
||||
// Set variant to RFC 4122 (10xx as the high bits of byte 8).
|
||||
u[8] = (u[8] & 0x3F) | 0x80
|
||||
|
||||
return u
|
||||
}(),
|
||||
wantTime: refTime,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "Version 4 UUID (not time-based)",
|
||||
uuid: func() UUID {
|
||||
var u UUID
|
||||
// Set first 6 bytes to same timestamp to verify it's ignored.
|
||||
copy(u[:6], timestampBytes)
|
||||
// Set version to 4 (0100 as the high nibble of byte 6).
|
||||
u[6] = (u[6] & 0x0F) | 0x40
|
||||
|
||||
return u
|
||||
}(),
|
||||
wantTime: time.Time{}, // Zero time for non-V7 UUIDs
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "Zero UUID",
|
||||
uuid: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
wantTime: time.Time{}, // Zero time for version 0
|
||||
wantOk: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotTime, gotOk := tt.uuid.Time()
|
||||
|
||||
assert.Equal(t, tt.wantOk, gotOk)
|
||||
|
||||
if tt.wantTime.IsZero() {
|
||||
assert.True(t, gotTime.IsZero())
|
||||
} else {
|
||||
// Compare time at millisecond precision.
|
||||
assert.Equal(t, tt.wantTime.UnixMilli(), gotTime.UnixMilli())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID_Time(b *testing.B) {
|
||||
uuid, err := NewV7()
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = uuid.Time()
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUID_Version(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
uuid UUID
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "Version 0 (invalid/nil UUID)",
|
||||
uuid: UUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
want: 0,
|
||||
},
|
||||
{
|
||||
name: "Version 1 (time-based)",
|
||||
uuid: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0x10, 0xf0, // 0x10 = version 1 in top nibble
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
want: 1,
|
||||
},
|
||||
{
|
||||
name: "Version 2 (DCE Security)",
|
||||
uuid: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0x20, 0xf0, // 0x20 = version 2 in top nibble
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
want: 2,
|
||||
},
|
||||
{
|
||||
name: "Version 3 (name-based, MD5)",
|
||||
uuid: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0x30, 0xf0, // 0x30 = version 3 in top nibble
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
want: 3,
|
||||
},
|
||||
{
|
||||
name: "Version 4 (random)",
|
||||
uuid: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0x40, 0xf0, // 0x40 = version 4 in top nibble
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
want: 4,
|
||||
},
|
||||
{
|
||||
name: "Version 5 (name-based, SHA-1)",
|
||||
uuid: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0x50, 0xf0, // 0x50 = version 5 in top nibble
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "Version 7 (time-ordered)",
|
||||
uuid: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0x70, 0xf0, // 0x70 = version 7 in top nibble
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
want: 7,
|
||||
},
|
||||
{
|
||||
name: "Version 8 (custom)",
|
||||
uuid: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0x80, 0xf0, // 0x80 = version 8 in top nibble
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
want: 8,
|
||||
},
|
||||
{
|
||||
name: "Version 15 (theoretical max)",
|
||||
uuid: UUID{
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
0x9a, 0xbc,
|
||||
0xf0, 0xf0, // 0xf0 = version 15 in top nibble
|
||||
0x12, 0x34,
|
||||
0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0,
|
||||
},
|
||||
want: 15,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.uuid.Version()
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID_Version(b *testing.B) {
|
||||
uuid, err := NewV7()
|
||||
require.NoError(b, err)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = uuid.Version()
|
||||
}
|
||||
}
|
||||
56
uuid/version_7.go
Normal file
56
uuid/version_7.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewV7 returns a time-ordered UUID v7 in string format.
|
||||
//
|
||||
// The UUID v7 format uses a timestamp with millisecond precision in the most
|
||||
// significant bits, followed by random data. This provides both uniqueness and
|
||||
// chronological ordering, making it ideal for database primary keys and
|
||||
// situations where sorting by creation time is desired.
|
||||
//
|
||||
// References:
|
||||
// - https://uuid7.com/
|
||||
// - https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
|
||||
//
|
||||
//nolint:lll
|
||||
func NewV7() (UUID, error) {
|
||||
var u UUID
|
||||
|
||||
// Write the timestamp to the first 6 bytes of the UUID.
|
||||
timestamp := time.Now().UnixMilli()
|
||||
u[0] = byte(timestamp >> 40)
|
||||
u[1] = byte(timestamp >> 32)
|
||||
u[2] = byte(timestamp >> 24)
|
||||
u[3] = byte(timestamp >> 16)
|
||||
u[4] = byte(timestamp >> 8)
|
||||
u[5] = byte(timestamp)
|
||||
|
||||
// Fill the remaining bytes with random data.
|
||||
_, err := rand.Read(u[6:])
|
||||
if err != nil {
|
||||
// This should never happen.
|
||||
return u, err
|
||||
}
|
||||
|
||||
// Set the version and variant bits.
|
||||
u[6] = (u[6] & 0x0f) | 0x70 // Version: 7 (time-ordered)
|
||||
u[8] = (u[8] & 0x3f) | 0x80 // Variant: RFC 4122
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// V7Time returns the time of a UUIDv7 string.
|
||||
//
|
||||
// If the UUID is not a valid UUIDv7, it returns a zero time and false.
|
||||
func V7Time(s string) (t time.Time, ok bool) {
|
||||
u, err := FromString(s)
|
||||
if err != nil {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
return u.Time()
|
||||
}
|
||||
168
uuid/version_7_test.go
Normal file
168
uuid/version_7_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUUIDv7(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
m := regexp.MustCompile(
|
||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||
)
|
||||
|
||||
// Store timestamps to verify they're increasing.
|
||||
var lastTimestampBytes int64
|
||||
var lastUUID string
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
for i := 0; i < 10000; i++ {
|
||||
got, err := NewV7()
|
||||
require.NoError(t, err)
|
||||
require.Regexp(t, m, got.String())
|
||||
|
||||
if _, ok := seen[got.String()]; ok {
|
||||
require.FailNow(t, "duplicate UUID")
|
||||
}
|
||||
|
||||
seen[got.String()] = struct{}{}
|
||||
|
||||
// Check version is 7.
|
||||
require.Equal(t, 7, int(got[6]>>4), "version is not 7")
|
||||
|
||||
// Check variant is RFC 4122.
|
||||
require.Equal(t, byte(0x80), got[8]&0xc0, "variant is not RFC 4122")
|
||||
|
||||
// Extract timestamp bytes.
|
||||
timestampBytes := int64(got[0])<<40 | int64(got[1])<<32 |
|
||||
int64(got[2])<<24 | int64(got[3])<<16 | int64(got[4])<<8 |
|
||||
int64(got[5])
|
||||
|
||||
// Verify timestamp is within 10 seconds of current time. This is a
|
||||
// sanity check to ensure the UUID is not too far off from the current
|
||||
// time, while allowing tests to pass on super slow machines.
|
||||
tsTime := time.UnixMilli(timestampBytes)
|
||||
require.WithinDuration(t, time.Now(), tsTime, 10*time.Second,
|
||||
"timestamp is not within 10 seconds of current time",
|
||||
)
|
||||
|
||||
// After the first UUID, verify that UUIDs are monotonically increasing
|
||||
if i > 0 && timestampBytes < lastTimestampBytes {
|
||||
require.FailNow(t, "UUIDs are not monotonically increasing",
|
||||
"current: %s (ts: %d), previous: %s (ts: %d)",
|
||||
got, timestampBytes, lastUUID, lastTimestampBytes)
|
||||
}
|
||||
|
||||
lastTimestampBytes = timestampBytes
|
||||
lastUUID = got.String()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewV7(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = NewV7()
|
||||
}
|
||||
}
|
||||
|
||||
func TestV7Time(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Define a reference time for testing.
|
||||
refTime := time.Date(2023, 5, 15, 12, 0, 0, 0, time.UTC)
|
||||
// The timestamp in milliseconds.
|
||||
timestampMillis := refTime.UnixMilli()
|
||||
|
||||
// Create bytes for the timestamp (first 6 bytes of UUID).
|
||||
timestampBytes := []byte{
|
||||
byte(timestampMillis >> 40),
|
||||
byte(timestampMillis >> 32),
|
||||
byte(timestampMillis >> 24),
|
||||
byte(timestampMillis >> 16),
|
||||
byte(timestampMillis >> 8),
|
||||
byte(timestampMillis),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
uuidStr string
|
||||
wantTime time.Time
|
||||
wantOk bool
|
||||
}{
|
||||
{
|
||||
name: "Version 7 UUID",
|
||||
uuidStr: func() string {
|
||||
var u UUID
|
||||
// Set first 6 bytes to timestamp.
|
||||
copy(u[:6], timestampBytes)
|
||||
// Set version to 7 (0111 as the high nibble of byte 6).
|
||||
u[6] = (u[6] & 0x0F) | 0x70
|
||||
// Set variant to RFC 4122 (10xx as the high bits of byte 8).
|
||||
u[8] = (u[8] & 0x3F) | 0x80
|
||||
|
||||
return u.String()
|
||||
}(),
|
||||
wantTime: refTime,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "Version 4 UUID (not time-based)",
|
||||
uuidStr: func() string {
|
||||
var u UUID
|
||||
// Set first 6 bytes to same timestamp to verify it's ignored.
|
||||
copy(u[:6], timestampBytes)
|
||||
// Set version to 4 (0100 as the high nibble of byte 6).
|
||||
u[6] = (u[6] & 0x0F) | 0x40
|
||||
// Set variant to RFC 4122 (10xx as the high bits of byte 8).
|
||||
u[8] = (u[8] & 0x3F) | 0x80
|
||||
|
||||
return u.String()
|
||||
}(),
|
||||
wantTime: time.Time{}, // Zero time for non-V7 UUIDs
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "Zero UUID",
|
||||
uuidStr: "00000000-0000-0000-0000-000000000000",
|
||||
wantTime: time.Time{}, // Zero time for version 0
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid UUID string",
|
||||
uuidStr: "not-a-valid-uuid",
|
||||
wantTime: time.Time{},
|
||||
wantOk: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotTime, gotOk := V7Time(tt.uuidStr)
|
||||
|
||||
assert.Equal(t, tt.wantOk, gotOk)
|
||||
|
||||
if tt.wantTime.IsZero() {
|
||||
assert.True(t, gotTime.IsZero())
|
||||
} else {
|
||||
// Compare time at millisecond precision.
|
||||
assert.Equal(t, tt.wantTime.UnixMilli(), gotTime.UnixMilli())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkV7Time(b *testing.B) {
|
||||
u, err := NewV7()
|
||||
require.NoError(b, err)
|
||||
|
||||
s := u.String()
|
||||
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
_, _ = V7Time(s)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user