initial commit

This commit is contained in:
2026-01-18 13:37:49 -05:00
parent d68799eacd
commit 8d6882ac0d
3 changed files with 209 additions and 0 deletions

122
ids.go Normal file
View File

@@ -0,0 +1,122 @@
package smolid
import (
"encoding/base32"
"encoding/binary"
"fmt"
"math/rand/v2"
"strings"
"time"
)
// Taking a reference to time.Now to make testing easier
var now = time.Now
const (
epoch uint64 = 1735707600000 // 2025-01-01 00:00:00
timestampShiftOffset = 23 // The timestamp is shifted three bytes to the left
versionShiftOffset = 21
versionMask = 0b11 << versionShiftOffset
v1TypeShiftOffset = 9
v1TypeFlag = 0b1 << 20
v1TypeMask = 0b111 << v1TypeShiftOffset
v1RandomSpace = 0xfffff // In V1, the least significant two and a half bytes (20 bits) can be random
v1Version = 0b1 << 21
)
/*
ID is a 64-bit (8-byte) value intended to be
- URL-Friendly; short and unobtrusive in its default unpadded base32 string encoding
- temporally sortable with strong index locality
- fast-enough and unique-enough for most use cases
Field Definitions
- Timestamp (41 bits) The most significant 41 bits represent a millisecond-precision timestamp
The allowed timestamp range is 2025-01-01 00:00:00 - 2094-09-07 15:47:35
- Version (2 bits) Bits 41-42 are reserved for versioning.
v1 is 0b01
- Type Flag (1 bit): Bit 43 serves as a boolean flag. If set, the "Type/Rand" field (Byte 6) is an embedded type
identifier.
- Reserved/Random (4 bits): The remaining 4 bits of the 6th byte are currently reserved. In Version 1, these are
populated with pseudo-random data.
- Type/Random (7 bits): If the Type Flag is set, this field contains the Type Identifier. Otherwise, it
is populated with pseudo-random data.
- Random (9 bits): The remaining byte is dedicated to pseudo-random data to reasonably ensure uniqueness.
String Format
The string format is base32 with no padding. Canonically the string is lowercased. This decision is purely for
aesthetics, but the parser is case-insensitive and will accept uppercase base32 strings.
*/
type ID struct {
n uint64
}
func New() ID {
var id = (uint64(now().UTC().UnixMilli()) - epoch) << timestampShiftOffset // set the timestamp
id |= v1Version // set the version bit
id |= rand.Uint64N(v1RandomSpace) // radom-fill the remaining space
return ID{id}
}
func Nil() ID { return ID{0} }
func NewForType(typ byte) ID {
id := New() // get a new v1 ID
id.n &^= v1TypeMask // clear the random data in the type space
id.n |= v1TypeFlag // set the type flag
id.n |= uint64(typ) << v1TypeShiftOffset // set the type
return id
}
func FromString(s string) (_ ID, err error) {
defer func() {
if r := recover(); r != nil {
var ok bool
err, ok = r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
}
}()
s = strings.ToUpper(s)
var bs []byte
bs, err = base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(s)
if err != nil {
return Nil(), err
}
return ID{binary.BigEndian.Uint64(bs)}, nil
}
func Must(id ID, err error) ID {
if err != nil {
panic("couldn't parse id: " + err.Error())
}
return id
}
func (id ID) String() string {
return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(id.Bytes()))
}
func (id ID) Version() int { return int(id.n&versionMask) >> versionShiftOffset }
func (id ID) Bytes() []byte {
var bs = make([]byte, 8)
binary.BigEndian.PutUint64(bs, id.n)
return bs
}
func (id ID) Time() time.Time {
var ms = int64(id.n>>timestampShiftOffset + epoch) // extract the timestamp
return time.Unix(ms/1000, (ms%1000)*int64(time.Millisecond)) // fill into a time.Time
}
func (id ID) Type() byte {
typ := id.n & v1TypeMask
return byte(typ >> v1TypeShiftOffset)
}