Skip to content

goland-express/snowflake

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Discord Snowflake

A high-performance, zero-dependency Go library for working with Discord snowflake IDs.

Go Reference Go Report Card Coverage

Installation

go get github.com/goland-express/snowflake

Quick Start

package main

import (
    "fmt"
    "time"
    "github.com/goland-express/snowflake"
)

func main() {
    // Parse a snowflake from string
    id, _ := snowflake.Parse("175928847299117063")
    
    // Extract timestamp
    fmt.Println("Created at:", id.Time())
    // Output: Created at: 2016-04-30 11:18:25.796 -0300 -03
    
    // Get components
    fmt.Println("Worker ID:", id.WorkerID())
    fmt.Println("Process ID:", id.ProcessID())
    fmt.Println("Sequence:", id.Sequence())
    
    // Create a new snowflake
    newID := snowflake.NewFromTimestamp(time.Now())
    
    // Compare snowflakes
    if id.Before(newID) {
        fmt.Println("id was created before newID")
    }
}

API Overview

Parsing

// Parse from string
id, err := snowflake.Parse("175928847299117063")

// Parse with validation (checks if within Discord's valid range)
id, err := snowflake.ParseStrict("175928847299117063")

// Parse and panic on error
id := snowflake.MustParse("175928847299117063")

// Parse from environment variable
id := snowflake.Getenv("DISCORD_CHANNEL_ID")
id, found := snowflake.LookupEnv("DISCORD_CHANNEL_ID")

Creating

// Create from timestamp (worker, process, sequence = 0)
id := snowflake.NewFromTimestamp(time.Now())

// Create with all components
id := snowflake.New(time.Now(), workerID, processID, sequence)

// Create a range for time-based queries
minID, maxID := snowflake.NewRange(startTime, endTime)
// Use in SQL: WHERE id BETWEEN minID AND maxID

Extracting Information

// Get timestamp
timestamp := id.Time()

// Get individual components
workerID := id.WorkerID()    // 5 bits
processID := id.ProcessID()  // 5 bits
sequence := id.Sequence()    // 12 bits

// Get all at once
deconstructed := id.Deconstruct()
fmt.Printf("Time: %v, Worker: %d, Process: %d, Seq: %d\n",
    deconstructed.Time,
    deconstructed.WorkerID,
    deconstructed.ProcessID,
    deconstructed.Sequence)

Validation

// Check if zero
if id.IsZero() {
    fmt.Println("Empty snowflake")
}

// Check if non-zero
if id.Valid() {
    fmt.Println("Valid snowflake")
}

// Check if within Discord's theoretical valid range
if id.IsValid() {
    fmt.Println("Valid Discord snowflake")
}

Comparison

id1 := snowflake.ID(100)
id2 := snowflake.ID(200)

// Compare
if id1.Before(id2) { }
if id1.After(id2) { }
if id1.Equal(id2) { }

// Three-way comparison (-1, 0, 1)
result := id1.Compare(id2)

// Check if in range (inclusive)
if id.IsBetween(minID, maxID) { }

JSON

type Message struct {
    ID      snowflake.ID `json:"id"`
    Content string       `json:"content"`
}

// Marshals as string: {"id":"175928847299117063"}
msg := Message{ID: 175928847299117063, Content: "Hello"}
json.Marshal(msg)

// Unmarshals from string or number
json.Unmarshal([]byte(`{"id":"175928847299117063"}`), &msg)
json.Unmarshal([]byte(`{"id":175928847299117063}`), &msg)

// Allow unquoted numbers (off by default)
snowflake.AllowUnquoted = true

String Conversion

// Convert to string
str := id.String()

// Implements fmt.Stringer
fmt.Printf("ID: %s\n", id)

Constants

snowflake.Epoch                  // Discord epoch (1420070400000)
snowflake.MinID                  // Minimum possible ID (0)
snowflake.MaxID                  // Maximum possible ID
snowflake.FirstDiscordSnowflake  // First real Discord snowflake (175928847299117063)

Snowflake Format

Discord snowflakes are 64-bit unsigned integers with the following structure:

 64                                          22     17     12          0
 ┌───────────────────────────────────────────┬──────┬──────┬──────────┐
 │         Timestamp (42 bits)               │Worker│Process│Increment│
 │    Milliseconds since Discord Epoch       │ (5)  │ (5)  │  (12)    │
 └───────────────────────────────────────────┴──────┴──────┴──────────┘
  • Timestamp (42 bits): Milliseconds since Discord Epoch (January 1, 2015)
  • Worker ID (5 bits): Internal worker ID (0-31)
  • Process ID (5 bits): Internal process ID (0-31)
  • Increment (12 bits): Sequence number (0-4095) for IDs generated in the same millisecond

Performance

Benchmarks:

BenchmarkParse-12              100000000    11.72 ns/op    0 B/op    0 allocs/op
BenchmarkNew-12               1000000000     0.24 ns/op    0 B/op    0 allocs/op
BenchmarkString-12              61432237    19.20 ns/op    0 B/op    0 allocs/op
BenchmarkTime-12              1000000000     0.24 ns/op    0 B/op    0 allocs/op
BenchmarkMarshalJSON-12         58816952    20.28 ns/op    0 B/op    0 allocs/op
BenchmarkUnmarshalJSON-12       44850418    26.43 ns/op    0 B/op    0 allocs/op
BenchmarkComparison-12        1000000000     0.24 ns/op    0 B/op    0 allocs/op

Use Cases

Time-based Queries

// Get all messages from yesterday
start := time.Now().Add(-24 * time.Hour)
end := time.Now()
minID, maxID := snowflake.NewRange(start, end)

// SQL: SELECT * FROM messages WHERE id BETWEEN ? AND ?
db.Query("SELECT * FROM messages WHERE id BETWEEN ? AND ?", minID, maxID)

Sorting

// Sort messages by snowflake (chronologically)
sort.Slice(messages, func(i, j int) bool {
    return messages[i].ID.Before(messages[j].ID)
})

Validation

// Validate user input
id, err := snowflake.ParseStrict(userInput)
if err != nil {
    return fmt.Errorf("invalid Discord ID: %w", err)
}

Contributing

Pull requests are welcome. For major changes, open an issue first to discuss what you want to change.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages