mirror of
https://github.com/jimeh/rands.git
synced 2026-02-19 11:26:38 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
312e856234 | ||
|
f4dccc6e46
|
|||
|
ec94efe49e
|
|||
| fe4308607c | |||
| e87d9c4726 | |||
| 127ebbaa03 | |||
|
|
41227bd53d | ||
| 16bd3ea3b9 | |||
| 8da5e1ef80 | |||
|
1b0bb32a3e
|
3
.github/.release-please-manifest.json
vendored
Normal file
3
.github/.release-please-manifest.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "0.4.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"
|
||||
}
|
||||
106
.github/workflows/ci.yml
vendored
106
.github/workflows/ci.yml
vendored
@@ -7,11 +7,14 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
version: v1.43
|
||||
go-version-file: "go.mod"
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.64
|
||||
env:
|
||||
VERBOSE: "true"
|
||||
|
||||
@@ -19,34 +22,22 @@ jobs:
|
||||
name: Tidy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version-file: "go.mod"
|
||||
- name: Check if mods are tidy
|
||||
run: make check-tidy
|
||||
|
||||
benchmark:
|
||||
name: Benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref != 'refs/heads/master'
|
||||
if: github.ref != 'refs/heads/main'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version-file: "go.mod"
|
||||
- name: Run benchmarks
|
||||
run: make bench | tee output.raw
|
||||
- name: Fix benchmark names
|
||||
@@ -67,18 +58,12 @@ jobs:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version-file: "go.mod"
|
||||
- name: Publish coverage
|
||||
uses: paambaati/codeclimate-action@v2.7.4
|
||||
uses: paambaati/codeclimate-action@v9
|
||||
env:
|
||||
VERBOSE: "true"
|
||||
GOMAXPROCS: 4
|
||||
@@ -92,17 +77,22 @@ jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version:
|
||||
- "1.17"
|
||||
- "1.18"
|
||||
- "1.19"
|
||||
- "1.20"
|
||||
- "1.21"
|
||||
- "1.22"
|
||||
- "1.23"
|
||||
- "1.24"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Run tests
|
||||
run: make test
|
||||
env:
|
||||
@@ -111,18 +101,15 @@ jobs:
|
||||
benchmark-store:
|
||||
name: Store benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/master'
|
||||
if: github.ref == 'refs/heads/main'
|
||||
permissions:
|
||||
deployments: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.15
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
go-version-file: "go.mod"
|
||||
- name: Run benchmarks
|
||||
run: make bench | tee output.raw
|
||||
- name: Fix benchmark names
|
||||
@@ -134,6 +121,19 @@ jobs:
|
||||
with:
|
||||
tool: "go"
|
||||
output-file-path: output.txt
|
||||
github-token: ${{ secrets.GH_PUSH_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
comment-on-alert: true
|
||||
auto-push: true
|
||||
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
outputs:
|
||||
release_created: ${{ steps.release-please.outputs.release_created }}
|
||||
version: ${{ steps.release-please.outputs.version }}
|
||||
steps:
|
||||
- uses: jimeh/release-please-manifest-action@v2
|
||||
id: release-please
|
||||
with:
|
||||
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
|
||||
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
|
||||
|
||||
@@ -4,18 +4,13 @@ linters-settings:
|
||||
statements: 150
|
||||
gocyclo:
|
||||
min-complexity: 20
|
||||
golint:
|
||||
min-confidence: 0
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
lll:
|
||||
line-length: 80
|
||||
tab-width: 4
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
@@ -24,13 +19,11 @@ linters:
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- depguard
|
||||
- copyloopvar
|
||||
- durationcheck
|
||||
- errcheck
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- funlen
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
@@ -58,12 +51,10 @@ linters:
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -2,6 +2,18 @@
|
||||
|
||||
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.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)
|
||||
|
||||
|
||||
|
||||
35
Makefile
35
Makefile
@@ -34,23 +34,19 @@ SHELL := env \
|
||||
# Tools
|
||||
#
|
||||
|
||||
TOOLS += $(TOOLDIR)/gobin
|
||||
$(TOOLDIR)/gobin:
|
||||
GO111MODULE=off go get -u github.com/myitcv/gobin
|
||||
|
||||
# external tool
|
||||
define tool # 1: binary-name, 2: go-import-path
|
||||
TOOLS += $(TOOLDIR)/$(1)
|
||||
|
||||
$(TOOLDIR)/$(1): $(TOOLDIR)/gobin Makefile
|
||||
gobin $(V) "$(2)"
|
||||
$(TOOLDIR)/$(1): Makefile
|
||||
GOBIN="$(CURDIR)/$(TOOLDIR)" go install "$(2)"
|
||||
endef
|
||||
|
||||
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc))
|
||||
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt))
|
||||
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports))
|
||||
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.43))
|
||||
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod))
|
||||
$(eval $(call tool,godoc,golang.org/x/tools/cmd/godoc@latest))
|
||||
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
|
||||
$(eval $(call tool,goimports,golang.org/x/tools/cmd/goimports@latest))
|
||||
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64))
|
||||
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest))
|
||||
|
||||
.PHONY: tools
|
||||
tools: $(TOOLS)
|
||||
@@ -158,20 +154,3 @@ check-tidy:
|
||||
docs: $(TOOLDIR)/godoc
|
||||
$(info serviing docs on http://127.0.0.1:6060/pkg/$(GOMODNAME)/)
|
||||
@godoc -http=127.0.0.1:6060
|
||||
|
||||
#
|
||||
# Release
|
||||
#
|
||||
|
||||
.PHONY: new-version
|
||||
new-version: check-npx
|
||||
npx standard-version
|
||||
|
||||
.PHONY: next-version
|
||||
next-version: check-npx
|
||||
npx standard-version --dry-run
|
||||
|
||||
.PHONY: check-npx
|
||||
check-npx:
|
||||
$(if $(shell which npx),,\
|
||||
$(error No npx found in PATH, please install NodeJS))
|
||||
|
||||
36
README.md
36
README.md
@@ -11,29 +11,13 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://pkg.go.dev/github.com/jimeh/rands">
|
||||
<img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white"
|
||||
alt="Go Reference">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/releases">
|
||||
<img src="https://img.shields.io/github/v/tag/jimeh/rands?label=release" alt="GitHub tag (latest SemVer)">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/actions">
|
||||
<img src="https://img.shields.io/github/workflow/status/jimeh/rands/CI.svg?logo=github" alt="Actions Status">
|
||||
</a>
|
||||
<a href="https://codeclimate.com/github/jimeh/rands">
|
||||
<img src="https://img.shields.io/codeclimate/coverage/jimeh/rands.svg?logo=code%20climate" alt="Coverage">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/issues">
|
||||
<img src="https://img.shields.io/github/issues-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white"
|
||||
alt="GitHub issues">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/pulls">
|
||||
<img src="https://img.shields.io/github/issues-pr-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests">
|
||||
</a>
|
||||
<a href="https://github.com/jimeh/rands/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/jimeh/rands.svg?style=flat" alt="License Status">
|
||||
</a>
|
||||
<a href="https://pkg.go.dev/github.com/jimeh/rands"><img src="https://img.shields.io/badge/%E2%80%8B-reference-387b97.svg?logo=go&logoColor=white" alt="Go Reference"></a>
|
||||
<a href="https://github.com/jimeh/rands/releases"><img src="https://img.shields.io/github/v/tag/jimeh/rands?label=release" alt="GitHub tag (latest SemVer)"></a>
|
||||
<a href="https://github.com/jimeh/rands/actions"><img src="https://img.shields.io/github/actions/workflow/status/jimeh/rands/ci.yml?branch=main&logo=github" alt="Actions Status"></a>
|
||||
<a href="https://codeclimate.com/github/jimeh/rands"><img src="https://img.shields.io/codeclimate/coverage/jimeh/rands.svg?logo=code%20climate" alt="Coverage"></a>
|
||||
<a href="https://github.com/jimeh/rands/issues"><img src="https://img.shields.io/github/issues-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub issues"></a>
|
||||
<a href="https://github.com/jimeh/rands/pulls"><img src="https://img.shields.io/github/issues-pr-raw/jimeh/rands.svg?style=flat&logo=github&logoColor=white" alt="GitHub pull requests"></a>
|
||||
<a href="https://github.com/jimeh/rands/blob/master/LICENSE"><img src="https://img.shields.io/github/license/jimeh/rands.svg?style=flat" alt="License Status"></a>
|
||||
</p>
|
||||
|
||||
## [`rands`](https://pkg.go.dev/github.com/jimeh/rands) package
|
||||
@@ -69,6 +53,7 @@ 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
|
||||
@@ -76,7 +61,7 @@ n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
|
||||
b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63]
|
||||
```
|
||||
|
||||
## [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/must) package
|
||||
## [`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.
|
||||
@@ -111,6 +96,7 @@ 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
|
||||
@@ -123,7 +109,7 @@ b := randsmust.Bytes(8) // => [205 128 54 95 0 95 53 51]
|
||||
Please see the Go Reference for documentation and examples:
|
||||
|
||||
- [`rands`](https://pkg.go.dev/github.com/jimeh/rands)
|
||||
- [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/must)
|
||||
- [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/randsmust)
|
||||
|
||||
## Benchmarks
|
||||
|
||||
|
||||
10
go.mod
10
go.mod
@@ -1,5 +1,11 @@
|
||||
module github.com/jimeh/rands
|
||||
|
||||
go 1.15
|
||||
go 1.17
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
16
go.sum
16
go.sum
@@ -1,11 +1,19 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
16
ints.go
16
ints.go
@@ -8,13 +8,13 @@ import (
|
||||
|
||||
var ErrInvalidMaxInt = fmt.Errorf("%w: max cannot be less than 1", Err)
|
||||
|
||||
// Int generates a random int ranging between 0 and max.
|
||||
func Int(max int) (int, error) {
|
||||
if max < 1 {
|
||||
// Int generates a random int ranging between 0 and nMax.
|
||||
func Int(nMax int) (int, error) {
|
||||
if nMax < 1 {
|
||||
return 0, ErrInvalidMaxInt
|
||||
}
|
||||
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(int64(nMax)))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -22,13 +22,13 @@ func Int(max int) (int, error) {
|
||||
return int(r.Int64()), nil
|
||||
}
|
||||
|
||||
// Int64 generates a random int64 ranging between 0 and max.
|
||||
func Int64(max int64) (int64, error) {
|
||||
if max < 1 {
|
||||
// Int64 generates a random int64 ranging between 0 and nMax.
|
||||
func Int64(nMax int64) (int64, error) {
|
||||
if nMax < 1 {
|
||||
return 0, ErrInvalidMaxInt
|
||||
}
|
||||
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(max))
|
||||
r, err := rand.Int(rand.Reader, big.NewInt(nMax))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package randsmust
|
||||
|
||||
import "github.com/jimeh/rands"
|
||||
|
||||
// Int generates a random int ranging between 0 and max.
|
||||
func Int(max int) int {
|
||||
r, err := rands.Int(max)
|
||||
// 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)
|
||||
}
|
||||
@@ -12,9 +12,9 @@ func Int(max int) int {
|
||||
return r
|
||||
}
|
||||
|
||||
// Int64 generates a random int64 ranging between 0 and max.
|
||||
func Int64(max int64) int64 {
|
||||
r, err := rands.Int64(max)
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -165,12 +165,12 @@ func UnicodeString(n int, alphabet []rune) string {
|
||||
//
|
||||
// In summary, the generated random string will:
|
||||
//
|
||||
// - be between 1 and 63 characters in length, other n values returns a error
|
||||
// - first character will be one of a-z
|
||||
// - last character will be one of a-z or 0-9
|
||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||
// - be between 1 and 63 characters in length, other n values returns a error
|
||||
// - first character will be one of a-z
|
||||
// - last character will be one of a-z or 0-9
|
||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||
func DNSLabel(n int) string {
|
||||
r, err := rands.DNSLabel(n)
|
||||
if err != nil {
|
||||
@@ -180,7 +180,7 @@ func DNSLabel(n int) string {
|
||||
return r
|
||||
}
|
||||
|
||||
// UUID returns a random UUID v4 in string format as defined by RFC 4122,
|
||||
// UUIDv4 returns a random UUID v4 in string format as defined by RFC 4122,
|
||||
// section 4.4.
|
||||
func UUID() string {
|
||||
r, err := rands.UUID()
|
||||
@@ -190,3 +190,24 @@ func UUID() string {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -75,3 +75,8 @@ 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
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jimeh/rands"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -487,19 +488,82 @@ func TestUUID(t *testing.T) {
|
||||
`^[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",
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51
strings.go
51
strings.go
@@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"unicode"
|
||||
|
||||
"github.com/jimeh/rands/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,7 +20,6 @@ const (
|
||||
alphabeticChars = upperChars + lowerChars
|
||||
alphanumericChars = alphabeticChars + numericChars
|
||||
dnsLabelChars = lowerNumericChars + "-"
|
||||
uuidHyphen = byte('-')
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -171,12 +172,12 @@ func UnicodeString(n int, alphabet []rune) (string, error) {
|
||||
//
|
||||
// In summary, the generated random string will:
|
||||
//
|
||||
// - be between 1 and 63 characters in length, other n values returns a error
|
||||
// - first character will be one of a-z
|
||||
// - last character will be one of a-z or 0-9
|
||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||
// - be between 1 and 63 characters in length, other n values returns a error
|
||||
// - first character will be one of a-z
|
||||
// - last character will be one of a-z or 0-9
|
||||
// - in-between first and last characters consist of a-z, 0-9, or "-"
|
||||
// - potentially contain two or more consecutive "-", except the 3rd and 4th
|
||||
// characters, as that would violate RFC 5891, section 4.2.3.1.
|
||||
func DNSLabel(n int) (string, error) {
|
||||
switch {
|
||||
case n < 1 || n > 63:
|
||||
@@ -237,27 +238,33 @@ func DNSLabel(n int) (string, error) {
|
||||
// UUID returns a random UUID v4 in string format as defined by RFC 4122,
|
||||
// section 4.4.
|
||||
func UUID() (string, error) {
|
||||
b, err := Bytes(16)
|
||||
uuid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b[6] = (b[6] & 0x0f) | 0x40 // Version: 4 (random)
|
||||
b[8] = (b[8] & 0x3f) | 0x80 // Variant: RFC 4122
|
||||
return uuid.String(), nil
|
||||
}
|
||||
|
||||
// Construct a UUID v4 string according to RFC 4122 specifications.
|
||||
dst := make([]byte, 36)
|
||||
hex.Encode(dst[0:8], b[0:4]) // time-low
|
||||
dst[8] = uuidHyphen
|
||||
hex.Encode(dst[9:13], b[4:6]) // time-mid
|
||||
dst[13] = uuidHyphen
|
||||
hex.Encode(dst[14:18], b[6:8]) // time-high-and-version
|
||||
dst[18] = uuidHyphen
|
||||
hex.Encode(dst[19:23], b[8:10]) // clock-seq-and-reserved, clock-seq-low
|
||||
dst[23] = uuidHyphen
|
||||
hex.Encode(dst[24:], b[10:]) // node
|
||||
// UUIDv7 returns a time-ordered UUID v7 in string format.
|
||||
//
|
||||
// The UUID v7 format uses a timestamp with millisecond precision in the most
|
||||
// significant bits, followed by random data. This provides both uniqueness and
|
||||
// chronological ordering, making it ideal for database primary keys and
|
||||
// situations where sorting by creation time is desired.
|
||||
//
|
||||
// References:
|
||||
// - https://uuid7.com/
|
||||
// - https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
|
||||
//
|
||||
//nolint:lll
|
||||
func UUIDv7() (string, error) {
|
||||
uuid, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(dst), nil
|
||||
return uuid.String(), nil
|
||||
}
|
||||
|
||||
func isASCII(s string) bool {
|
||||
|
||||
@@ -132,3 +132,12 @@ func ExampleUUID() {
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -612,20 +613,26 @@ func TestUUID(t *testing.T) {
|
||||
`^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`,
|
||||
)
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
got, err := UUID()
|
||||
require.NoError(t, err)
|
||||
require.Regexp(t, m, got)
|
||||
|
||||
if _, ok := seen[got]; ok {
|
||||
require.FailNow(t, "duplicate UUID")
|
||||
}
|
||||
|
||||
seen[got] = struct{}{}
|
||||
|
||||
raw := strings.ReplaceAll(got, "-", "")
|
||||
b := make([]byte, 16)
|
||||
_, err = hex.Decode(b, []byte(raw))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 4, int(b[6]>>4), "version is not 4")
|
||||
require.Equal(t, byte(0x80), b[8]&0xc0,
|
||||
"variant is not RFC 4122",
|
||||
)
|
||||
require.Equal(t, byte(0x80), b[8]&0xc0, "variant is not RFC 4122")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,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
|
||||
//
|
||||
|
||||
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