Swift: UUID to odd or even number
Video: Swift: UUID to odd or even number by Taught by Celeste AI - AI Coding Coach
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.