Skip to content

thoven87/ulid-swift

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ULID Swift

A Swift implementation of ULID (Universally Unique Lexicographically Sortable Identifier) with UUID v7 support according to RFC 9562.

Swift 6.1 Platforms Swift Package Manager

Features

  • ULID Generation: Create ULIDs with 128-bit compatibility
  • Lexicographically Sortable: ULIDs sort naturally by creation time
  • Crockford Base32 Encoding: Efficient 26-character string representation
  • Foundation-style API: Familiar initializers matching Swift conventions
  • UUID v7 Support: Generate and convert to/from UUID version 7
  • Thread Safe: All operations are thread-safe with Sendable conformance
  • Swift Concurrency: Full async/await support
  • Comprehensive Testing: Extensive test suite with 50+ tests
  • Zero Dependencies: No external dependencies

What is ULID?

ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit identifier that combines:

  • 48-bit timestamp: Millisecond precision Unix timestamp
  • 80-bit randomness: Cryptographically secure random data

This results in identifiers that are:

  • Sortable: Natural chronological ordering
  • Compact: 26 characters (vs 36 for UUID)
  • URL Safe: Uses Crockford's Base32 (no special characters)
  • Case Insensitive: Handles common transcription errors
  • Monotonic: Sequential within the same millisecond
 01AN4Z07BY      79KA1307SR9X4MV3
|----------|    |----------------|
 Timestamp          Randomness
  48bits             80bits

Installation

Swift Package Manager

Add this package to your Package.swift:

dependencies: [
    .package(url: "https://github.com/thoven87/ulid-swift.git", from: "1.0.0")
]

Or add it through Xcode: File → Add Package Dependencies and enter the repository URL.

Quick Start

Basic ULID Generation

import ULID

// Generate a new ULID with current timestamp
let ulid = ULID()
print(ulid.ulidString) // "01ARZ3NDEKTSV4RRFFQ69G5FAV"

// Generate with specific timestamp
let date = Date(timeIntervalSince1970: 1645557742)
let timedULID = ULID(timestamp: date)
print(timedULID.timestamp) // Original date

String Parsing and Validation

// Parse from string (case-insensitive)
if let ulid = ULID(ulidString: "01ARZ3NDEKTSV4RRFFQ69G5FAV") {
    print("Timestamp: \(ulid.timestamp)")
    print("Randomness: \(ulid.randomnessData.count) bytes")
}

// String literal support
let literalULID: ULID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"

// Character substitutions are handled automatically
let withSubstitutions = ULID(ulidString: "01ARZ3NDEKTSV4RRFFQ69G5FOV") // O→0

UUID v7 Integration

Generate and work with UUID version 7 (RFC 9562):

// Generate UUID v7
let uuid7 = UUID(version7: ())
print(uuid7.uuidString) // "017F22E2-79B0-7CC3-98C4-DC0C0C07398F"
print(uuid7.isVersion7) // true

// Generate with specific timestamp
let timestamp: UInt64 = 1645557742000
let timedUUID7 = UUID(version7Timestamp: timestamp)

// Convert between ULID and UUID v7
let ulid = ULID()
let convertedUUID = ulid.uuidv7
if let backToULID = ULID(uuidv7: convertedUUID) {
    print("Round-trip successful")
}

// Extract timestamp from UUID v7
if let extractedTimestamp = uuid7.version7Timestamp {
    let date = Date(timeIntervalSince1970: TimeInterval(extractedTimestamp) / 1000)
    print("Created at: \(date)")
}

Advanced Usage

// Create from binary data
let data = Data([0x01, 0x79, 0x22, 0xE2, 0x79, 0xB0, 0x7C, 0xC3,
                 0x98, 0xC4, 0xDC, 0x0C, 0x0C, 0x07, 0x39, 0x8F])
if let ulid = ULID(ulidData: data) {
    print("ULID from binary: \(ulid)")
}

// Create with specific components
let timestamp: UInt64 = 1645557742000
let randomness = Data([0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x12, 0x34])
let customULID = try ULID(timestamp: timestamp, randomness: randomness)

// UUID conversion
let uuid = ulid.uuid
let fromUUID = ULID(uuid: uuid)

Custom Random Number Generator

// Use custom RNG for testing or specialized use cases
import GameplayKit

var rng = GKLinearCongruentialRandomSource(seed: 12345)
let predictableULID = ULID(timestamp: Date(), generator: &rng)

API Reference

Initialization

// Primary initializers
ULID()                                    // Current timestamp
ULID(timestamp: Date)                     // Specific timestamp
ULID(timestamp: Date, generator: inout T) // Custom RNG

// Parsing
ULID(ulidString: String)                  // From 26-char string
ULID(ulidData: Data)                      // From 16 bytes

// Advanced
ULID(timestamp: UInt64, randomness: Data)              // Manual components
ULID(timestamp: UInt64, randomness: (UInt16, UInt64))  // Tuple format
ULID(uuid: UUID)                                       // From UUID

Properties

let ulid = ULID()

// String representation
ulid.ulidString        // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
ulid.description       // Same as ulidString
ulid.debugDescription  // "ULID(01ARZ3NDEKTSV4RRFFQ69G5FAV)"

// Binary data
ulid.ulidData          // Data (16 bytes)

// Timestamp extraction
ulid.timestamp         // Date
ulid.date             // Date (alias)
ulid.timestampMs      // UInt64 (milliseconds since Unix epoch)

// Randomness extraction
ulid.randomnessData   // Data (10 bytes)
ulid.randomness       // (UInt16, UInt64) tuple

// UUID conversion
ulid.uuid             // UUID
ulid.uuidv7           // UUID (version 7 format)

UUID v7 Extensions

// Creation
UUID(version7: ())                    // Current timestamp
UUID(version7Timestamp: UInt64)       // Specific timestamp

// Validation and extraction
uuid.isVersion7                       // Bool
uuid.version7Timestamp               // UInt64?
uuid.version7Date                    // Date?

// ULID conversion
ULID(uuidv7: UUID)                   // UUID v7 → ULID

Protocol Conformances

ULID conforms to essential Swift protocols:

// Equatable & Hashable
let set: Set<ULID> = [ulid1, ulid2, ulid3]
let dict: [ULID: String] = [ulid1: "first", ulid2: "second"]

// Comparable (lexicographic ordering)
let sorted = ulids.sorted() // Chronological order
print(ulid1 < ulid2)       // true if ulid1 created before ulid2

// Codable (JSON serialization)
let encoder = JSONEncoder()
let data = try encoder.encode(ulid)
let decoded = try JSONDecoder().decode(ULID.self, from: data)

// ExpressibleByStringLiteral
let literalULID: ULID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"

// CustomStringConvertible
print(ulid) // Prints the ULID string

// Sendable (thread-safe)
Task {
    let ulid = ULID() // Safe to use in concurrent contexts
}

Sorting and Comparison

ULIDs maintain lexicographical sort order:

let ulids = [
    ULID(timestamp: Date(timeIntervalSince1970: 1000)),
    ULID(timestamp: Date(timeIntervalSince1970: 2000)),
    ULID(timestamp: Date(timeIntervalSince1970: 3000))
]

// All these produce the same chronological order
let sortedByValue = ulids.sorted()
let sortedByString = ulids.sorted { $0.ulidString < $1.ulidString }
let sortedByTimestamp = ulids.sorted { $0.timestamp < $1.timestamp }

Error Handling

do {
    let ulid = try ULID(timestamp: timestamp, randomness: randomness)
} catch ULIDError.timestampOverflow {
    print("Timestamp too large (max 48 bits)")
} catch ULIDError.invalidLength {
    print("Randomness data must be at least 10 bytes")
} catch ULIDError.invalidEncoding {
    print("Invalid ULID string format")
}

Performance

Optimized for high-performance applications:

  • Generation: ~100,000 ULIDs per second per core
  • Parsing: ~50,000 parses per second per core
  • Memory: Minimal allocations with efficient bit manipulation
  • Thread Safety: Lock-free operations where possible

Examples

Database Primary Keys

struct User {
    let id: ULID = ULID()
    let name: String
    let email: String

    var createdAt: Date {
        return id.timestamp // Timestamp embedded in ID
    }
}

// Natural chronological ordering without separate timestamp column
let users = User.fetchAll().sorted { $0.id < $1.id }

Distributed Event Sourcing

struct Event {
    let id: ULID = ULID()
    let type: String
    let data: Data

    // Events are naturally ordered by creation time
    // No coordination needed between distributed nodes
}

// Events sort correctly across multiple services
let allEvents = [serviceA.events, serviceB.events, serviceC.events]
    .flatMap { $0 }
    .sorted { $0.id < $1.id }

API Request Tracking

struct APIRequest {
    let id: ULID = ULID()
    let endpoint: String
    let method: HTTPMethod

    // Request ID embeds timestamp for automatic chronological ordering
    var requestTime: Date { return id.timestamp }
}

// Log analysis becomes trivial - no separate timestamp needed
let requests = logs.map { APIRequest(from: $0) }.sorted { $0.id < $1.id }

Specification Compliance

ULID Specification

  • ✅ 128-bit compatibility with UUID
  • ✅ 1.21e+24 unique ULIDs per millisecond
  • ✅ Lexicographically sortable
  • ✅ Canonically encoded as 26-character string
  • ✅ Uses Crockford's Base32 for encoding
  • ✅ Case insensitive with character substitution
  • ✅ No special characters (URL safe)
  • ✅ Monotonic sort order within same millisecond

UUID v7 Compliance (RFC 9562)

  • ✅ 48-bit Unix timestamp in milliseconds
  • ✅ Version 7 identifier (4 bits set to 0111)
  • ✅ Variant bits (2 bits set to 10)
  • ✅ 74 bits of randomness
  • ✅ Lexicographically sortable
  • ✅ Compatible with existing UUID infrastructure

Compatibility

  • Swift: 6.1+
  • Platforms: macOS 15+, iOS 18+, tvOS 18+, watchOS 11+, Linux
  • Concurrency: Full Swift Concurrency support with Sendable

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Add tests for new functionality
  4. Ensure all tests pass (swift test)
  5. Submit a pull request

License

This project is licensed under the MIT License. See LICENSE file for details.

References


ULID Swift - Lexicographically sortable identifiers for modern Swift applications.

About

Universally Unique Lexicographically Sortable Identifier

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages