Part of Swift with Copilot

Swift: UUID to odd or even number

Sandy LaneSandy Lane

Video: Swift: UUID to odd or even number by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Swift with Copilot: UUID to Odd or Even Number

UUID().hashValue % 2 == 0 — odd/even based on hash. Or sum the hex digits and check parity. Useful for A/B test bucketing without storing extra data — partition users by their existing UUID.

The use case: you've got a UUID per user, and you want to deterministically split them into two groups (50/50, A/B test). Hash to int, take parity.

The Copilot prompt

// Convert a UUID to an odd or even classification
import Foundation

Copilot completes:

import Foundation

let uuid = UUID()
print("UUID: \(uuid)")

let isEven = uuid.hashValue % 2 == 0
print(isEven ? "Even" : "Odd")

Output (varies):

UUID: A1B2C3D4-E5F6-7890-ABCD-EF1234567890
Even

What's a UUID?

A 128-bit random identifier. Standard format: 8-4-4-4-12 hex digits.

let uuid = UUID()
print(uuid)
// "550E8400-E29B-41D4-A716-446655440000"

UUID() generates a v4 (random) UUID — 122 bits of entropy. Practically unique.

hashValue method

let h = uuid.hashValue
print(h)    // some Int

hashValue returns an Int. It's not stable across runs (Swift randomizes hash seeds). For an A/B bucket that should persist, hashing UUID isn't right — because the same UUID hashes differently on different launches.

Stable parity from a UUID

Better approach: derive parity from the UUID's bytes directly.

import Foundation

extension UUID {
  var isEven: Bool {
    return self.uuid.0 % 2 == 0
  }
}

let uuid = UUID()
print(uuid.isEven ? "Even" : "Odd")

UUID.uuid is a tuple of 16 UInt8 bytes. Take the first byte; even → bucket A, odd → bucket B.

This is deterministic — same UUID always gives the same answer.

Sum-of-hex-digits parity

extension UUID {
  var hexSumParity: Int {
    let hex = self.uuidString.replacingOccurrences(of: "-", with: "")
    let sum = hex.compactMap { Int(String($0), radix: 16) }.reduce(0, +)
    return sum % 2
  }
}

let uuid = UUID()
print(uuid.hexSumParity)

Same idea — deterministic, distributed roughly 50/50.

When NOT to use UUID for bucketing

  • Sequential UUIDs. Some apps generate "v1" UUIDs (timestamp-based). Sequential bytes can correlate with time, skewing buckets.
  • External UUIDs. If users get their UUID from another system, you don't control its randomness properties.
  • Small samples. Random ≠ uniform for small N. A/B tests with thousands of users → fine. Tens → not fine.

For larger experiments, use a hash function over (UUID + experiment ID):

import CryptoKit

extension UUID {
  func bucket(for experiment: String, mod: Int = 100) -> Int {
    let key = experiment + uuidString
    let hash = SHA256.hash(data: Data(key.utf8))
    let firstByte = hash.first ?? 0
    return Int(firstByte) % mod
  }
}

let uuid = UUID()
let bucket = uuid.bucket(for: "homepage_redesign", mod: 2)
print(bucket)    // 0 or 1, deterministic per (UUID, experiment)

CryptoKit.SHA256 ensures uniform distribution. Adding the experiment ID prevents correlation across simultaneous experiments.

Even from any Hashable

For non-UUID types:

extension Hashable {
  func parity() -> Int {
    abs(hashValue) % 2
  }
}

Same caveat: hashValue isn't stable across runs. For persistent bucketing, hash explicitly:

import CryptoKit

func stableHash(_ s: String) -> Int {
  let bytes = SHA256.hash(data: Data(s.utf8))
  return bytes.withUnsafeBytes { $0.load(as: Int.self) }
}

A complete A/B helper

import Foundation
import CryptoKit

enum Bucket: String {
  case control, treatment
}

func bucket(userId: UUID, experiment: String) -> Bucket {
  let key = experiment + ":" + userId.uuidString
  let hash = SHA256.hash(data: Data(key.utf8))
  let byte = hash.first ?? 0
  return byte < 128 ? .control : .treatment
}

let user = UUID()
print(bucket(userId: user, experiment: "new-onboarding"))

Stable, uniformly distributed, per-experiment independent. The pattern any A/B framework uses.

Common stumbles

hashValue isn't stable. Same UUID, different runs → different hash. Don't use for persistent bucketing.

UUID v1 vs v4. UUID() in Swift is v4 (random). Other systems may produce v1 (timestamp). Random properties differ.

Tiny samples. Bucketing 10 users 50/50 might give 7/3. That's still random — don't blame the bucketer.

Mixing experiments. If you bucket by raw UUID, every experiment has the same buckets — users in "control" for one are likely "control" for all. Salt with experiment ID.

Modular bias. byte % 100 slightly biases (256 isn't a multiple of 100). For 50/50 it's fine; for fine-grained percentages, use rejection sampling or scale fairly.

Privacy. UUIDs persist across app launches; mapping to real users is your responsibility (GDPR, etc.).

What's next

Episode 19: Swap random elements in array.

Recap

UUIDs → parity via byte access (uuid.uuid.0 % 2) is deterministic. hashValue is not stable across runs — don't use for persistent bucketing. For A/B tests, hash (experiment + uuid) with SHA256, take a byte, apply your bucket logic. Salt by experiment to keep groups independent.

Next episode: random swap.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.