Skip to content

Repository files navigation

DeterministicShuffleKit

🌐 Language: Deutsch | English | Español | Français

Swift 6.0+ Platforms SPM Compatible License: MIT

A Swift package that orders any collection so it looks shuffled yet stays perfectly reproducible: same candidates, same context, same order — every time, on every device. Zero dependencies, Sendable by design, protocol-oriented API.

Documentation

Features

  • 🎲 Deterministic, not random: the same candidates and the same context always produce the same order — no seed to store, nothing to persist
  • 🧩 Context-scoped: the very same candidates produce an independent, reproducible order for every context ("home", "recommendations", ...)
  • Stable under change: removing a candidate, then adding it back, returns it to its original position relative to the others
  • 📦 Zero dependencies: no CryptoKit, no Foundation-only hashing, no third-party package — a small hand-rolled FNV-1a hash is the only moving part
  • 🧵 Sendable by design: a stateless value type; a single instance can be shared and reused across concurrent calls
  • 🔌 Three ways to identify a candidate: protocol conformance, a KeyPath, or a closure — pick whichever fits your type
  • 📖 Fully documented: DocC comments on the whole public API
  • 🧪 Thoroughly tested: Swift Testing suites covering ordering guarantees, all three API entry points, and error paths

Installation

Swift Package Manager

Add to your Package.swift:

dependencies: [
    .package(url: "https://github.com/hoseiocean/DeterministicShuffleKit.git", from: "1.0.0")
]

Or in Xcode: File → Add Package Dependencies → Enter the repository URL:

https://github.com/hoseiocean/DeterministicShuffleKit.git

Usage Examples

Conforming to DeterministicShuffleIdentifiable

import DeterministicShuffle

struct Product: DeterministicShuffleIdentifiable {
    let sku: String
    let name: String

    var shuffleIdentifier: String { sku }
}

let shuffler = DeterministicShuffler()
let products: [Product] = [
    Product(sku: "sneakers", name: "Sneakers"),
    Product(sku: "boots", name: "Boots"),
    Product(sku: "sandals", name: "Sandals")
]

let home = try shuffler.shuffled(products, context: "home")
let recommendations = try shuffler.shuffled(products, context: "recommendations")
// `home` and `recommendations` hold the same products in two different,
// independently reproducible orders.

Using a KeyPath

Use this when a type you don't own — or shouldn't make conform — already has a suitable identifier:

struct Article {
    let slug: String
    let title: String
}

let articles: [Article] = [...]
let ordered = try shuffler.shuffled(
    articles,
    context: "technology",
    identifiedBy: \.slug
)

Using a closure

Use this when no single property is a unique identifier on its own:

struct Company {
    let exchange: String
    let symbol: String
}

let companies: [Company] = [...]
let ordered = try shuffler.shuffled(
    companies,
    context: "featured"
) { company in
    "\(company.exchange):\(company.symbol)"
}

Handling errors

do {
    let ordered = try shuffler.shuffled(products, context: "home")
} catch DeterministicShuffleError.emptyIdentifier {
    // A candidate's identifier was blank.
} catch DeterministicShuffleError.duplicateIdentifier(let identifier) {
    // Two candidates shared `identifier`.
}

How It Works

Each candidate is identified by a non-empty, unique string. That identifier is combined with a DeterministicShuffleContext and hashed with FNV-1a, a fast, seedless, non-cryptographic hash — the resulting order is the candidates sorted by that hash.

This gives three guarantees:

  • Reproducible: the hash is a pure function of the context and the identifier, so the same inputs always produce the same order, across runs and devices
  • Context-scoped: a different context changes every hash, and therefore the whole order, without touching the candidates themselves
  • Locally stable: each candidate's position is computed independently of the others, so adding or removing a candidate never reshuffles the rest — removing one and adding it back returns it to its original position

On the rare occasion two candidates hash to the same value, the order falls back to comparing their identifiers directly, so the result stays fully deterministic even then.

Requirements

  • Swift 6.0+
  • iOS 13+ / macOS 10.15+

Contributing

Contributions are welcome! Please read our Contributing Guide before contributing to understand our development process and design decisions.

License

MIT License - see LICENSE for details.

About

A Swift package that orders any collection so it looks shuffled yet stays perfectly reproducible: same candidates, same context, same order, every time, on every device. Zero dependencies, Sendable by design.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages