mirror of
https://github.com/jimeh/rands.git
synced 2026-02-19 03:16:39 +00:00
Compare commits
21 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
|
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
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: golangci-lint
|
- uses: actions/setup-go@v5
|
||||||
uses: golangci/golangci-lint-action@v2
|
|
||||||
with:
|
with:
|
||||||
version: v1.35
|
go-version-file: "go.mod"
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: v1.64
|
||||||
env:
|
env:
|
||||||
VERBOSE: "true"
|
VERBOSE: "true"
|
||||||
|
|
||||||
@@ -19,34 +22,22 @@ jobs:
|
|||||||
name: Tidy
|
name: Tidy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version-file: "go.mod"
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Check if mods are tidy
|
- name: Check if mods are tidy
|
||||||
run: make check-tidy
|
run: make check-tidy
|
||||||
|
|
||||||
benchmark:
|
benchmark:
|
||||||
name: Benchmarks
|
name: Benchmarks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.ref != 'refs/heads/master'
|
if: github.ref != 'refs/heads/main'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version-file: "go.mod"
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Run benchmarks
|
- name: Run benchmarks
|
||||||
run: make bench | tee output.raw
|
run: make bench | tee output.raw
|
||||||
- name: Fix benchmark names
|
- name: Fix benchmark names
|
||||||
@@ -67,18 +58,12 @@ jobs:
|
|||||||
name: Coverage
|
name: Coverage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version-file: "go.mod"
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Publish coverage
|
- name: Publish coverage
|
||||||
uses: paambaati/codeclimate-action@v2.7.4
|
uses: paambaati/codeclimate-action@v9
|
||||||
env:
|
env:
|
||||||
VERBOSE: "true"
|
VERBOSE: "true"
|
||||||
GOMAXPROCS: 4
|
GOMAXPROCS: 4
|
||||||
@@ -92,17 +77,23 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Test
|
name: Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go-version:
|
||||||
|
- "1.18"
|
||||||
|
- "1.19"
|
||||||
|
- "1.20"
|
||||||
|
- "1.21"
|
||||||
|
- "1.22"
|
||||||
|
- "1.23"
|
||||||
|
- "1.24"
|
||||||
|
- "stable"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version: ${{ matrix.go-version }}
|
||||||
- uses: actions/cache@v2
|
check-latest: true
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
env:
|
env:
|
||||||
@@ -111,18 +102,15 @@ jobs:
|
|||||||
benchmark-store:
|
benchmark-store:
|
||||||
name: Store benchmarks
|
name: Store benchmarks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/main'
|
||||||
|
permissions:
|
||||||
|
deployments: write
|
||||||
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.15
|
go-version-file: "go.mod"
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-
|
|
||||||
- name: Run benchmarks
|
- name: Run benchmarks
|
||||||
run: make bench | tee output.raw
|
run: make bench | tee output.raw
|
||||||
- name: Fix benchmark names
|
- name: Fix benchmark names
|
||||||
@@ -134,6 +122,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tool: "go"
|
tool: "go"
|
||||||
output-file-path: output.txt
|
output-file-path: output.txt
|
||||||
github-token: ${{ secrets.GH_PUSH_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
comment-on-alert: true
|
comment-on-alert: true
|
||||||
auto-push: 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
|
statements: 150
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 20
|
min-complexity: 20
|
||||||
golint:
|
|
||||||
min-confidence: 0
|
|
||||||
govet:
|
govet:
|
||||||
check-shadowing: true
|
|
||||||
enable-all: true
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- fieldalignment
|
||||||
lll:
|
lll:
|
||||||
line-length: 80
|
line-length: 80
|
||||||
tab-width: 4
|
tab-width: 4
|
||||||
maligned:
|
|
||||||
suggest-new: true
|
|
||||||
misspell:
|
misspell:
|
||||||
locale: US
|
locale: US
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
disable-all: true
|
disable-all: true
|
||||||
enable:
|
enable:
|
||||||
|
- asciicheck
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- deadcode
|
- copyloopvar
|
||||||
- depguard
|
- durationcheck
|
||||||
- dupl
|
|
||||||
- errcheck
|
- errcheck
|
||||||
|
- errorlint
|
||||||
|
- exhaustive
|
||||||
- funlen
|
- funlen
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- goerr113
|
- godot
|
||||||
|
- gofumpt
|
||||||
- goimports
|
- goimports
|
||||||
- golint
|
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
|
- importas
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- lll
|
- lll
|
||||||
- misspell
|
- misspell
|
||||||
- nakedret
|
- nakedret
|
||||||
|
- nilerr
|
||||||
- nlreturn
|
- nlreturn
|
||||||
- noctx
|
- noctx
|
||||||
- nolintlint
|
- nolintlint
|
||||||
- scopelint
|
- prealloc
|
||||||
|
- predeclared
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- structcheck
|
|
||||||
- typecheck
|
- typecheck
|
||||||
- unconvert
|
- unconvert
|
||||||
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- varcheck
|
- wastedassign
|
||||||
- whitespace
|
- whitespace
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
include:
|
|
||||||
# - EXC0002 # disable excluding of issues about comments from golint
|
|
||||||
exclude:
|
exclude:
|
||||||
- Using the variable on range scope `tt` in function literal
|
- Using the variable on range scope `tt` in function literal
|
||||||
- Using the variable on range scope `tc` in function literal
|
- Using the variable on range scope `tc` in function literal
|
||||||
@@ -71,6 +73,12 @@ issues:
|
|||||||
- source: "`json:"
|
- source: "`json:"
|
||||||
linters:
|
linters:
|
||||||
- lll
|
- lll
|
||||||
|
- source: "`xml:"
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
|
- source: "`yaml:"
|
||||||
|
linters:
|
||||||
|
- lll
|
||||||
|
|
||||||
run:
|
run:
|
||||||
timeout: 2m
|
timeout: 2m
|
||||||
|
|||||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -2,6 +2,37 @@
|
|||||||
|
|
||||||
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.
|
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)
|
## [0.2.0](https://github.com/jimeh/rands/compare/v0.1.1...v0.2.0) (2021-03-16)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
51
Makefile
51
Makefile
@@ -34,26 +34,19 @@ SHELL := env \
|
|||||||
# Tools
|
# Tools
|
||||||
#
|
#
|
||||||
|
|
||||||
TOOLS += $(TOOLDIR)/gobin
|
|
||||||
gobin: $(TOOLDIR)/gobin
|
|
||||||
$(TOOLDIR)/gobin:
|
|
||||||
GO111MODULE=off go get -u github.com/myitcv/gobin
|
|
||||||
|
|
||||||
# external tool
|
# external tool
|
||||||
define tool # 1: binary-name, 2: go-import-path
|
define tool # 1: binary-name, 2: go-import-path
|
||||||
TOOLS += $(TOOLDIR)/$(1)
|
TOOLS += $(TOOLDIR)/$(1)
|
||||||
|
|
||||||
.PHONY: $(1)
|
$(TOOLDIR)/$(1): Makefile
|
||||||
$(1): $(TOOLDIR)/$(1)
|
GOBIN="$(CURDIR)/$(TOOLDIR)" go install "$(2)"
|
||||||
|
|
||||||
$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile
|
|
||||||
gobin $(V) "$(2)"
|
|
||||||
endef
|
endef
|
||||||
|
|
||||||
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc))
|
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc@latest))
|
||||||
$(eval $(call tool,gofumports,mvdan.cc/gofumpt/gofumports))
|
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
|
||||||
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.35))
|
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports@latest))
|
||||||
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod))
|
$(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
|
.PHONY: tools
|
||||||
tools: $(TOOLS)
|
tools: $(TOOLS)
|
||||||
@@ -62,8 +55,8 @@ tools: $(TOOLS)
|
|||||||
# Development
|
# Development
|
||||||
#
|
#
|
||||||
|
|
||||||
TEST ?= $$(go list ./... | grep -v 'vendor')
|
|
||||||
BENCH ?= .
|
BENCH ?= .
|
||||||
|
TESTARGS ?=
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
@@ -72,24 +65,24 @@ clean:
|
|||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test:
|
test:
|
||||||
go test $(V) -count=1 $(TESTARGS) $(TEST)
|
go test $(V) -count=1 -race $(TESTARGS) ./...
|
||||||
|
|
||||||
.PHONY: test-deps
|
.PHONY: test-deps
|
||||||
test-deps:
|
test-deps:
|
||||||
go test all
|
go test all
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: golangci-lint
|
lint: $(TOOLDIR)/golangci-lint
|
||||||
GOGC=off golangci-lint $(V) run
|
golangci-lint $(V) run
|
||||||
|
|
||||||
.PHONY: format
|
.PHONY: format
|
||||||
format: gofumports
|
format: $(TOOLDIR)/goimports $(TOOLDIR)/gofumpt
|
||||||
gofumports -w .
|
goimports -w . && gofumpt -w .
|
||||||
|
|
||||||
.SILENT: bench
|
.SILENT: bench
|
||||||
.PHONY: bench
|
.PHONY: bench
|
||||||
bench:
|
bench:
|
||||||
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) $(TEST)
|
go test $(V) -count=1 -bench=$(BENCH) $(TESTARGS) ./...
|
||||||
|
|
||||||
#
|
#
|
||||||
# Coverage
|
# Coverage
|
||||||
@@ -115,16 +108,14 @@ coverage.out: $(SOURCES)
|
|||||||
|
|
||||||
.PHONY: deps
|
.PHONY: deps
|
||||||
deps:
|
deps:
|
||||||
$(info Downloading dependencies)
|
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
.PHONY: deps-update
|
.PHONY: deps-update
|
||||||
deps-update:
|
deps-update:
|
||||||
$(info Downloading dependencies)
|
go get -u -t ./...
|
||||||
go get -u ./...
|
|
||||||
|
|
||||||
.PHONY: deps-analyze
|
.PHONY: deps-analyze
|
||||||
deps-analyze: gomod
|
deps-analyze: $(TOOLDIR)/gomod
|
||||||
gomod analyze
|
gomod analyze
|
||||||
|
|
||||||
.PHONY: tidy
|
.PHONY: tidy
|
||||||
@@ -160,14 +151,6 @@ check-tidy:
|
|||||||
|
|
||||||
# Serve docs
|
# Serve docs
|
||||||
.PHONY: docs
|
.PHONY: docs
|
||||||
docs: godoc
|
docs: $(TOOLDIR)/godoc
|
||||||
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
|
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
|
||||||
@godoc -http=127.0.0.1:6060
|
@godoc -http=127.0.0.1:6060
|
||||||
|
|
||||||
#
|
|
||||||
# Release
|
|
||||||
#
|
|
||||||
|
|
||||||
.PHONY: new-version
|
|
||||||
new-version:
|
|
||||||
npx standard-version
|
|
||||||
|
|||||||
147
README.md
147
README.md
@@ -5,72 +5,117 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<strong>
|
||||||
Go package providing a suite of functions that use <code>crypto/rand</code>
|
Go package providing a suite of functions that use <code>crypto/rand</code>
|
||||||
to generate cryptographically secure random strings in various formats, as
|
to generate cryptographically secure random data in various forms and
|
||||||
well as ints and bytes.
|
formats.
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://pkg.go.dev/github.com/jimeh/rands">
|
<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>
|
||||||
<img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white"
|
<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>
|
||||||
alt="Go Reference">
|
<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>
|
<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/releases">
|
<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>
|
||||||
<img src="https://img.shields.io/github/v/tag/jimeh/rands?label=release" alt="GitHub tag (latest SemVer)">
|
<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>
|
<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://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>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
```go
|
## [`rands`](https://pkg.go.dev/github.com/jimeh/rands) package
|
||||||
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
|
|
||||||
|
|
||||||
s, _ := rands.String(16, "abcdefABCDEF") // => adCDCaDEdeffeDeb
|
`rands` is intended for use in production code where random data generation is
|
||||||
s, _ := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 下下口九力下土夕下土八上二夕大三
|
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
|
||||||
s, _ := rands.UUID() // => a62b8712-f238-43ba-a47e-333f5fffe785
|
with single return values, and they panic in the event of an error.
|
||||||
|
|
||||||
n, _ := rands.Int(2147483647) // => 1334400235
|
### Import
|
||||||
n, _ := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
|
|
||||||
|
|
||||||
b, _ := rands.Bytes(8) // => [0 220 137 243 135 204 34 63]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Import
|
|
||||||
|
|
||||||
```
|
```
|
||||||
import "github.com/jimeh/rands"
|
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
|
## Documentation
|
||||||
|
|
||||||
Please see the
|
Please see the Go Reference for documentation and examples:
|
||||||
[Go Reference](https://pkg.go.dev/github.com/jimeh/rands#section-documentation)
|
|
||||||
for documentation and examples.
|
- [`rands`](https://pkg.go.dev/github.com/jimeh/rands)
|
||||||
|
- [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/randsmust)
|
||||||
|
|
||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
@@ -79,4 +124,4 @@ https://jimeh.me/rands/dev/bench/
|
|||||||
|
|
||||||
## License
|
## 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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/jimeh/rands"
|
"github.com/jimeh/rands"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExampleBytes() {
|
func ExampleBytes() {
|
||||||
b, _ := rands.Bytes(8)
|
b, err := rands.Bytes(8)
|
||||||
fmt.Printf("%+v\n", b) // => [0 220 137 243 135 204 34 63]
|
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) {
|
func TestBytes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, _ := Bytes(tt.n)
|
got, _ := Bytes(tt.n)
|
||||||
|
|||||||
10
go.mod
10
go.mod
@@ -1,5 +1,11 @@
|
|||||||
module github.com/jimeh/rands
|
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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"
|
"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.
|
// Int generates a random int ranging between 0 and nMax.
|
||||||
func Int(max int) (int, error) {
|
func Int(nMax int) (int, error) {
|
||||||
if max < 1 {
|
if nMax < 1 {
|
||||||
return 0, errInvalidMaxInt
|
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 {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -22,13 +22,13 @@ func Int(max int) (int, error) {
|
|||||||
return int(r.Int64()), nil
|
return int(r.Int64()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Int64 generates a random int64 ranging between 0 and max.
|
// Int64 generates a random int64 ranging between 0 and nMax.
|
||||||
func Int64(max int64) (int64, error) {
|
func Int64(nMax int64) (int64, error) {
|
||||||
if max < 1 {
|
if nMax < 1 {
|
||||||
return 0, errInvalidMaxInt
|
return 0, ErrInvalidMaxInt
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := rand.Int(rand.Reader, big.NewInt(max))
|
r, err := rand.Int(rand.Reader, big.NewInt(nMax))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,25 @@ package rands_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/jimeh/rands"
|
"github.com/jimeh/rands"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExampleInt() {
|
func ExampleInt() {
|
||||||
n, _ := rands.Int(2147483647)
|
n, err := rands.Int(2147483647)
|
||||||
fmt.Printf("%d\n", n) // => 1334400235
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%d\n", n) // => 1908357440
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleInt64() {
|
func ExampleInt64() {
|
||||||
n, _ := rands.Int64(int64(9223372036854775807))
|
n, err := rands.Int64(int64(9223372036854775807))
|
||||||
fmt.Printf("%d\n", n) // => 8256935979116161233
|
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
|
package rands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -16,67 +15,67 @@ var testIntCases = []struct {
|
|||||||
{
|
{
|
||||||
name: "n=-2394345",
|
name: "n=-2394345",
|
||||||
max: -2394345,
|
max: -2394345,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=-409600",
|
name: "n=-409600",
|
||||||
max: -409600,
|
max: -409600,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=-1024",
|
name: "n=-1024",
|
||||||
max: -1024,
|
max: -1024,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=-128",
|
name: "n=-128",
|
||||||
max: -128,
|
max: -128,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=-32",
|
name: "n=-32",
|
||||||
max: -32,
|
max: -32,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=-16",
|
name: "n=-16",
|
||||||
max: -16,
|
max: -16,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=-8",
|
name: "n=-8",
|
||||||
max: -8,
|
max: -8,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=-7",
|
name: "n=-7",
|
||||||
max: -7,
|
max: -7,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=-2",
|
name: "n=-2",
|
||||||
max: -2,
|
max: -2,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=-1",
|
name: "n=-1",
|
||||||
max: -1,
|
max: -1,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=0",
|
name: "n=0",
|
||||||
max: 0,
|
max: 0,
|
||||||
errIs: errInvalidMaxInt,
|
errIs: ErrInvalidMaxInt,
|
||||||
errStr: "rands: max cannot be less than 1",
|
errStr: "rands: max cannot be less than 1",
|
||||||
},
|
},
|
||||||
{name: "n=1", max: 1},
|
{name: "n=1", max: 1},
|
||||||
@@ -92,6 +91,8 @@ var testIntCases = []struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInt(t *testing.T) {
|
func TestInt(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, tt := range testIntCases {
|
for _, tt := range testIntCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := Int(tt.max)
|
got, err := Int(tt.max)
|
||||||
@@ -102,7 +103,7 @@ func TestInt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tt.errIs != nil {
|
if tt.errIs != nil {
|
||||||
assert.True(t, errors.Is(err, errInvalidMaxInt))
|
assert.ErrorIs(t, err, tt.errIs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.errStr != "" {
|
if tt.errStr != "" {
|
||||||
@@ -123,6 +124,8 @@ func BenchmarkInt(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInt64(t *testing.T) {
|
func TestInt64(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, tt := range testIntCases {
|
for _, tt := range testIntCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := Int64(int64(tt.max))
|
got, err := Int64(int64(tt.max))
|
||||||
@@ -133,7 +136,7 @@ func TestInt64(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tt.errIs != nil {
|
if tt.errIs != nil {
|
||||||
assert.True(t, errors.Is(err, errInvalidMaxInt))
|
assert.ErrorIs(t, err, tt.errIs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.errStr != "" {
|
if tt.errStr != "" {
|
||||||
|
|||||||
9
rands.go
9
rands.go
@@ -4,8 +4,15 @@
|
|||||||
//
|
//
|
||||||
// All functions which produce strings from a alphabet of characters uses
|
// All functions which produce strings from a alphabet of characters uses
|
||||||
// rand.Int() to ensure a uniform distribution of all possible values.
|
// 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
|
package rands
|
||||||
|
|
||||||
import "errors"
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
63
strings.go
63
strings.go
@@ -7,6 +7,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/jimeh/rands/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -18,16 +20,15 @@ const (
|
|||||||
alphabeticChars = upperChars + lowerChars
|
alphabeticChars = upperChars + lowerChars
|
||||||
alphanumericChars = alphabeticChars + numericChars
|
alphanumericChars = alphabeticChars + numericChars
|
||||||
dnsLabelChars = lowerNumericChars + "-"
|
dnsLabelChars = lowerNumericChars + "-"
|
||||||
uuidHyphen = byte('-')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errNonASCIIAlphabet = fmt.Errorf(
|
ErrNonASCIIAlphabet = fmt.Errorf(
|
||||||
"%w: alphabet contains non-ASCII characters", errBase,
|
"%w: alphabet contains non-ASCII characters", Err,
|
||||||
)
|
)
|
||||||
|
|
||||||
errDNSLabelLength = fmt.Errorf(
|
ErrDNSLabelLength = fmt.Errorf(
|
||||||
"%w: DNS labels must be between 1 and 63 characters in length", errBase,
|
"%w: DNS labels must be between 1 and 63 characters in length", Err,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,7 +128,7 @@ func LowerNumeric(n int) (string, error) {
|
|||||||
// UnicodeString() if you need a alphabet with Unicode characters.
|
// UnicodeString() if you need a alphabet with Unicode characters.
|
||||||
func String(n int, alphabet string) (string, error) {
|
func String(n int, alphabet string) (string, error) {
|
||||||
if !isASCII(alphabet) {
|
if !isASCII(alphabet) {
|
||||||
return "", errNonASCIIAlphabet
|
return "", ErrNonASCIIAlphabet
|
||||||
}
|
}
|
||||||
|
|
||||||
l := big.NewInt(int64(len(alphabet)))
|
l := big.NewInt(int64(len(alphabet)))
|
||||||
@@ -171,16 +172,16 @@ func UnicodeString(n int, alphabet []rune) (string, error) {
|
|||||||
//
|
//
|
||||||
// In summary, the generated random string will:
|
// In summary, the generated random string will:
|
||||||
//
|
//
|
||||||
// - be between 1 and 63 characters in length, other n values returns a error
|
// - be between 1 and 63 characters in length, other n values returns a error
|
||||||
// - first character will be one of a-z
|
// - first character will be one of a-z
|
||||||
// - last character will be one of a-z or 0-9
|
// - last character will be one of a-z or 0-9
|
||||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||||
func DNSLabel(n int) (string, error) {
|
func DNSLabel(n int) (string, error) {
|
||||||
switch {
|
switch {
|
||||||
case n < 1 || n > 63:
|
case n < 1 || n > 63:
|
||||||
return "", errDNSLabelLength
|
return "", ErrDNSLabelLength
|
||||||
case n == 1:
|
case n == 1:
|
||||||
return String(1, lowerChars)
|
return String(1, lowerChars)
|
||||||
default:
|
default:
|
||||||
@@ -237,27 +238,33 @@ func DNSLabel(n int) (string, error) {
|
|||||||
// UUID returns a random UUID v4 in string format as defined by RFC 4122,
|
// UUID returns a random UUID v4 in string format as defined by RFC 4122,
|
||||||
// section 4.4.
|
// section 4.4.
|
||||||
func UUID() (string, error) {
|
func UUID() (string, error) {
|
||||||
b, err := Bytes(16)
|
uuid, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
b[6] = (b[6] & 0x0f) | 0x40 // Version: 4 (random)
|
return uuid.String(), nil
|
||||||
b[8] = (b[8] & 0x3f) | 0x80 // Variant: RFC 4122
|
}
|
||||||
|
|
||||||
// Construct a UUID v4 string according to RFC 4122 specifications.
|
// UUIDv7 returns a time-ordered UUID v7 in string format.
|
||||||
dst := make([]byte, 36)
|
//
|
||||||
hex.Encode(dst[0:8], b[0:4]) // time-low
|
// The UUID v7 format uses a timestamp with millisecond precision in the most
|
||||||
dst[8] = uuidHyphen
|
// significant bits, followed by random data. This provides both uniqueness and
|
||||||
hex.Encode(dst[9:13], b[4:6]) // time-mid
|
// chronological ordering, making it ideal for database primary keys and
|
||||||
dst[13] = uuidHyphen
|
// situations where sorting by creation time is desired.
|
||||||
hex.Encode(dst[14:18], b[6:8]) // time-high-and-version
|
//
|
||||||
dst[18] = uuidHyphen
|
// References:
|
||||||
hex.Encode(dst[19:23], b[8:10]) // clock-seq-and-reserved, clock-seq-low
|
// - https://uuid7.com/
|
||||||
dst[23] = uuidHyphen
|
// - https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
|
||||||
hex.Encode(dst[24:], b[10:]) // node
|
//
|
||||||
|
//nolint:lll
|
||||||
|
func UUIDv7() (string, error) {
|
||||||
|
uuid, err := uuid.NewV7()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
return string(dst), nil
|
return uuid.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isASCII(s string) bool {
|
func isASCII(s string) bool {
|
||||||
|
|||||||
@@ -2,76 +2,142 @@ package rands_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"github.com/jimeh/rands"
|
"github.com/jimeh/rands"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExampleBase64() {
|
func ExampleBase64() {
|
||||||
s, _ := rands.Base64(16)
|
s, err := rands.Base64(16)
|
||||||
fmt.Println(s) // => CYxqEdUB1Rzno3SyZu2g/g==
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => nYQLhIYTqh8oH/W4hZuXMQ==
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleBase64URL() {
|
func ExampleBase64URL() {
|
||||||
s, _ := rands.Base64URL(16)
|
s, err := rands.Base64URL(16)
|
||||||
fmt.Println(s) // => zlqw9aFqcFggbk2asn3_aQ
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => zI_zrc1l0uPT4MxncR6e5w
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleHex() {
|
func ExampleHex() {
|
||||||
s, _ := rands.Hex(16)
|
s, err := rands.Hex(16)
|
||||||
fmt.Println(s) // => 956e2ec9e7f19ddd58bb935826926531
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => b59e8977a13f3c030bd2ea1002ec8081
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleAlphanumeric() {
|
func ExampleAlphanumeric() {
|
||||||
s, _ := rands.Alphanumeric(16)
|
s, err := rands.Alphanumeric(16)
|
||||||
fmt.Println(s) // => Fvk1PkrmG5crgOjT
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => EgPieCBO7MuWhHtj
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleAlphabetic() {
|
func ExampleAlphabetic() {
|
||||||
s, _ := rands.Alphabetic(16)
|
s, err := rands.Alphabetic(16)
|
||||||
fmt.Println(s) // => XEJIzcZufHkuUmRM
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => VzcovEqvMRBWUtQC
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleUpper() {
|
func ExampleUpper() {
|
||||||
s, _ := rands.Upper(16)
|
s, err := rands.Upper(16)
|
||||||
fmt.Println(s) // => UMAGAFPPNDRGLUPZ
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => MCZEGPWGYKNUEDCK
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleUpperNumeric() {
|
func ExampleUpperNumeric() {
|
||||||
s, _ := rands.UpperNumeric(16)
|
s, err := rands.UpperNumeric(16)
|
||||||
fmt.Println(s) // => DF0CQS0TK9CPUO3E
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => 6LLPBBUW77B26X2X
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleLower() {
|
func ExampleLower() {
|
||||||
s, _ := rands.Lower(16)
|
s, err := rands.Lower(16)
|
||||||
fmt.Println(s) // => ocsmggykzrxzfwgt
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => dhoqhrqljadsztaa
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleLowerNumeric() {
|
func ExampleLowerNumeric() {
|
||||||
s, _ := rands.LowerNumeric(16)
|
s, err := rands.LowerNumeric(16)
|
||||||
fmt.Println(s) // => rwlv7a1p7klqffs5
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => th1z1b1d24l5h8pu
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleNumeric() {
|
func ExampleNumeric() {
|
||||||
s, _ := rands.Numeric(16)
|
s, err := rands.Numeric(16)
|
||||||
fmt.Println(s) // => 9403373143598295
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => 3378802228987741
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleString() {
|
func ExampleString() {
|
||||||
s, _ := rands.String(16, "abcdefABCDEF")
|
s, err := rands.String(16, "abcdefABCDEF")
|
||||||
fmt.Println(s) // => adCDCaDEdeffeDeb
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => BAFffADaadeeacfa
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleUnicodeString() {
|
func ExampleUnicodeString() {
|
||||||
s, _ := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大"))
|
s, err := rands.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大"))
|
||||||
fmt.Println(s) // => 下下口九力下土夕下土八上二夕大三
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => 八三口上土土七入力夕人力下三上力
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleDNSLabel() {
|
func ExampleDNSLabel() {
|
||||||
s, _ := rands.DNSLabel(16)
|
s, err := rands.DNSLabel(16)
|
||||||
fmt.Println(s) // => z0ij9o8qkbs0ru-h
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(s) // => ab-sbh5q0gfb6sqo
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleUUID() {
|
func ExampleUUID() {
|
||||||
s, _ := rands.UUID()
|
s, err := rands.UUID()
|
||||||
fmt.Println(s) // => a62b8712-f238-43ba-a47e-333f5fffe785
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
124
strings_test.go
124
strings_test.go
@@ -3,16 +3,18 @@ package rands
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHex(t *testing.T) {
|
func TestHex(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "0123456789abcdef"
|
allowed := "0123456789abcdef"
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
@@ -36,6 +38,8 @@ func BenchmarkHex(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBase64(t *testing.T) {
|
func TestBase64(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
|
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
|
||||||
"0123456789+/="
|
"0123456789+/="
|
||||||
|
|
||||||
@@ -63,6 +67,8 @@ func BenchmarkBase64(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBase64URL(t *testing.T) {
|
func TestBase64URL(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
|
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +
|
||||||
"0123456789-_"
|
"0123456789-_"
|
||||||
|
|
||||||
@@ -90,6 +96,8 @@ func BenchmarkBase64URL(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlphanumeric(t *testing.T) {
|
func TestAlphanumeric(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
@@ -113,6 +121,8 @@ func BenchmarkAlphanumeric(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlphabetic(t *testing.T) {
|
func TestAlphabetic(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
@@ -136,6 +146,8 @@ func BenchmarkAlphabetic(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestNumeric(t *testing.T) {
|
func TestNumeric(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "0123456789"
|
allowed := "0123456789"
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
@@ -159,6 +171,8 @@ func BenchmarkNumeric(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUpper(t *testing.T) {
|
func TestUpper(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
@@ -182,6 +196,8 @@ func BenchmarkUpper(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUpperNumeric(t *testing.T) {
|
func TestUpperNumeric(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
@@ -205,6 +221,8 @@ func BenchmarkUpperNumeric(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLower(t *testing.T) {
|
func TestLower(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "abcdefghijklmnopqrstuvwxyz"
|
allowed := "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
@@ -228,6 +246,8 @@ func BenchmarkLower(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLowerNumeric(t *testing.T) {
|
func TestLowerNumeric(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
allowed := "abcdefghijklmnopqrstuvwxyz0123456789"
|
allowed := "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
@@ -262,7 +282,7 @@ var stringTestCases = []struct {
|
|||||||
n: 32,
|
n: 32,
|
||||||
alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" +
|
alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" +
|
||||||
"άίόύώέϊϋΐΰΆΈΌΏΎΊ",
|
"άίόύώέϊϋΐΰΆΈΌΏΎΊ",
|
||||||
errIs: errNonASCIIAlphabet,
|
errIs: ErrNonASCIIAlphabet,
|
||||||
errStr: "rands: alphabet contains non-ASCII characters",
|
errStr: "rands: alphabet contains non-ASCII characters",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -270,7 +290,7 @@ var stringTestCases = []struct {
|
|||||||
n: 32,
|
n: 32,
|
||||||
alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" +
|
alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" +
|
||||||
"时道出而要于就下得可你年生",
|
"时道出而要于就下得可你年生",
|
||||||
errIs: errNonASCIIAlphabet,
|
errIs: ErrNonASCIIAlphabet,
|
||||||
errStr: "rands: alphabet contains non-ASCII characters",
|
errStr: "rands: alphabet contains non-ASCII characters",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -278,7 +298,7 @@ var stringTestCases = []struct {
|
|||||||
n: 32,
|
n: 32,
|
||||||
alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" +
|
alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" +
|
||||||
"手文日月木水火犬王正出本右四",
|
"手文日月木水火犬王正出本右四",
|
||||||
errIs: errNonASCIIAlphabet,
|
errIs: ErrNonASCIIAlphabet,
|
||||||
errStr: "rands: alphabet contains non-ASCII characters",
|
errStr: "rands: alphabet contains non-ASCII characters",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -349,6 +369,8 @@ var stringTestCases = []struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestString(t *testing.T) {
|
func TestString(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, tt := range stringTestCases {
|
for _, tt := range stringTestCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := String(tt.n, tt.alphabet)
|
got, err := String(tt.n, tt.alphabet)
|
||||||
@@ -359,7 +381,7 @@ func TestString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tt.errIs != nil {
|
if tt.errIs != nil {
|
||||||
assert.True(t, errors.Is(err, errNonASCIIAlphabet))
|
assert.ErrorIs(t, err, tt.errIs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.errStr != "" {
|
if tt.errStr != "" {
|
||||||
@@ -476,6 +498,8 @@ var unicodeStringTestCases = []struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnicodeString(t *testing.T) {
|
func TestUnicodeString(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, tt := range unicodeStringTestCases {
|
for _, tt := range unicodeStringTestCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, _ := UnicodeString(tt.n, []rune(tt.alphabet))
|
got, _ := UnicodeString(tt.n, []rune(tt.alphabet))
|
||||||
@@ -507,14 +531,14 @@ var dnsLabelTestCases = []struct {
|
|||||||
{
|
{
|
||||||
name: "n=-128",
|
name: "n=-128",
|
||||||
n: -128,
|
n: -128,
|
||||||
errIs: errDNSLabelLength,
|
errIs: ErrDNSLabelLength,
|
||||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||||
"in length",
|
"in length",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=0",
|
name: "n=0",
|
||||||
n: 0,
|
n: 0,
|
||||||
errIs: errDNSLabelLength,
|
errIs: ErrDNSLabelLength,
|
||||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||||
"in length",
|
"in length",
|
||||||
},
|
},
|
||||||
@@ -532,20 +556,22 @@ var dnsLabelTestCases = []struct {
|
|||||||
{
|
{
|
||||||
name: "n=64",
|
name: "n=64",
|
||||||
n: 64,
|
n: 64,
|
||||||
errIs: errDNSLabelLength,
|
errIs: ErrDNSLabelLength,
|
||||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||||
"in length",
|
"in length",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "n=128",
|
name: "n=128",
|
||||||
n: 128,
|
n: 128,
|
||||||
errIs: errDNSLabelLength,
|
errIs: ErrDNSLabelLength,
|
||||||
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
errStr: "rands: DNS labels must be between 1 and 63 characters " +
|
||||||
"in length",
|
"in length",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDNSLabel(t *testing.T) {
|
func TestDNSLabel(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
for _, tt := range dnsLabelTestCases {
|
for _, tt := range dnsLabelTestCases {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// generate lots of labels to increase the chances of catching any
|
// generate lots of labels to increase the chances of catching any
|
||||||
@@ -559,7 +585,7 @@ func TestDNSLabel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tt.errIs != nil {
|
if tt.errIs != nil {
|
||||||
require.True(t, errors.Is(err, errDNSLabelLength))
|
require.ErrorIs(t, err, tt.errIs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.errStr != "" {
|
if tt.errStr != "" {
|
||||||
@@ -581,24 +607,32 @@ func BenchmarkDNSLabel(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUUID(t *testing.T) {
|
func TestUUID(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
m := regexp.MustCompile(
|
m := regexp.MustCompile(
|
||||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
`^[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++ {
|
for i := 0; i < 10000; i++ {
|
||||||
got, err := UUID()
|
got, err := UUID()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Regexp(t, m, got)
|
require.Regexp(t, m, got)
|
||||||
|
|
||||||
|
if _, ok := seen[got]; ok {
|
||||||
|
require.FailNow(t, "duplicate UUID")
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[got] = struct{}{}
|
||||||
|
|
||||||
raw := strings.ReplaceAll(got, "-", "")
|
raw := strings.ReplaceAll(got, "-", "")
|
||||||
b := make([]byte, 16)
|
b := make([]byte, 16)
|
||||||
_, err = hex.Decode(b, []byte(raw))
|
_, err = hex.Decode(b, []byte(raw))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, 4, int(b[6]>>4), "version is not 4")
|
require.Equal(t, 4, int(b[6]>>4), "version is not 4")
|
||||||
require.Equal(t, byte(0x80), b[8]&0xc0,
|
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||||
"variant is not RFC 4122",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -608,6 +642,70 @@ func BenchmarkUUID(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// 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