From 2bf3590275465e23033f03a9556e3b94b7ddbf8c Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Wed, 18 Mar 2026 15:25:07 -0700 Subject: [PATCH 01/15] UUID v7 - Proposal, implementation, and performance enhancements --- .../Essentials/BenchmarkEssentials.swift | 28 +- Proposals/NNNN-uuid-versions.md | 259 ++++++++++++++ Sources/FoundationEssentials/UUID.swift | 316 +++++++++++++++--- .../FoundationEssentials/UUID_Wrappers.swift | 4 +- .../include/_FoundationCShims.h | 1 - Sources/_FoundationCShims/include/uuid.h | 81 ----- Sources/_FoundationCShims/uuid.c | 278 --------------- .../FoundationEssentialsTests/UUIDTests.swift | 252 ++++++++++++++ 8 files changed, 815 insertions(+), 404 deletions(-) create mode 100644 Proposals/NNNN-uuid-versions.md delete mode 100644 Sources/_FoundationCShims/include/uuid.h delete mode 100644 Sources/_FoundationCShims/uuid.c diff --git a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift index fcc25aab81..e9ef97d635 100644 --- a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift +++ b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift @@ -26,7 +26,7 @@ let benchmarks = { Benchmark.defaultConfiguration.metrics = [.cpuTotal, .wallClock, .throughput] // MARK: UUID - + Benchmark("UUIDEqual", configuration: .init(scalingFactor: .mega)) { benchmark in let u1 = UUID() let u2 = UUID() @@ -34,4 +34,30 @@ let benchmarks = { assert(u1 != u2) } } + + Benchmark("UUIDCreate") { benchmark in + for _ in benchmark.scaledIterations { + blackHole(UUID()) + } + } + + Benchmark("UUIDCreateTimeOrdered") { benchmark in + for _ in benchmark.scaledIterations { + blackHole(UUID.timeOrdered()) + } + } + + Benchmark("UUIDString") { benchmark in + let uuid = UUID() + for _ in benchmark.scaledIterations { + blackHole(uuid.uuidString) + } + } + + Benchmark("UUIDStringLower") { benchmark in + let uuid = UUID() + for _ in benchmark.scaledIterations { + blackHole(uuid.uuidStringLower) + } + } } diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md new file mode 100644 index 0000000000..0fa162b45f --- /dev/null +++ b/Proposals/NNNN-uuid-versions.md @@ -0,0 +1,259 @@ +# UUID Version Support and Other Enhancements + +* Proposal: [SF-NNNN](NNNN-uuid-versions.md) +* Authors: [Tony Parker](https://github.com/parkera) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: [swiftlang/swift-foundation#NNNNN](https://github.com/swiftlang/swift-foundation/pull/NNNNN) +* Review: ([pitch](https://forums.swift.org/...)) + +## Introduction + +Foundation's `UUID` type currently generates only version 4 (random) UUIDs. [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562) defines several UUID versions, each suited to different use cases. This proposal adds support for creating UUIDs of version 7 (time-ordered) which has become widely adopted for database keys and distributed systems due to its monotonically increasing, sortable nature. + +In addition, `UUID` is in need of a few more additions for modern usage, including support for lowercase strings, access to the bytes using `Span`, and accessors for the commonly used `nil` and `max` sentinel values. + +## Motivation + +UUID version 4 (random) is a good general-purpose identifier, but its randomness makes it poorly suited as a database primary key — inserts into B-tree indexes are scattered across the keyspace, leading to poor cache locality and increased write amplification. UUID version 7 addresses this by encoding a Unix timestamp in the most significant 48 bits, producing UUIDs that are monotonically increasing over time while retaining sufficient randomness for uniqueness. + +Today, developers who need time-ordered UUIDs usually construct the bytes manually using `UUID(uuid:)`, which is error-prone and requires understanding the RFC 9562 bit layout, or depend on another library. Foundation should provide a straightforward way to create version 7 UUIDs, and a general mechanism for version introspection that accommodates other UUID versions, even if we do not generate them in `UUID` itself. + +## Proposed solution + +Add a `UUID.Version` struct representing the well-known UUID versions from RFC 9562, a `version` property on `UUID` for introspection, a static factory method for creating version 7 UUIDs, and convenience properties for the nil and max UUIDs. + +```swift +// Create a time-ordered UUID +let id = UUID.timeOrdered() + +// Inspect the version of any UUID +switch id.version { +case .timeOrdered: + print("v7 UUID, sortable by creation time") +case .random: + print("v4 UUID") +default: + print("other version") +} + +// The existing init() continues to create version 4 UUIDs +let randomID = UUID() +assert(randomID.version == .random) + +// Nil and max UUIDs for sentinel values +let nilID = UUID.nil // 00000000-0000-0000-0000-000000000000 +let maxID = UUID.max // FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF + +// Access the raw bytes without copying +let uuid = UUID() +let span: Span = uuid.span // 16-element typed span +``` + +## Detailed design + +### Nil and Max UUIDs + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// The nil UUID, where all 128 bits are set to zero, as defined by + /// RFC 9562 Section 5.9. Can be used to represent the absence of a + /// UUID value. + public static let `nil`: UUID + + /// The max UUID, where all 128 bits are set to one, as defined by + /// RFC 9562 Section 5.10. Can be used as a sentinel value, for example + /// to represent "the largest possible UUID" in a sorted range. + public static let max: UUID +} +``` + +The nil UUID (`00000000-0000-0000-0000-000000000000`) and max UUID (`FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF`) are special forms defined by RFC 9562. They are useful as sentinel values — for example, representing "no UUID" or defining the bounds of a UUID range. Note that neither the nil UUID nor the max UUID has a meaningful version or variant field; the `version` property returns `Version(rawValue: 0)` and `Version(rawValue: 15)` respectively. + +### Lowercase string representation + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// Returns a lowercase string created from the UUID, such as + /// "e621e1f8-c36c-495a-93fc-0c247a3e6e5f". + public var uuidStringLower: String { get } +} +``` + +The existing `uuidString` property returns an uppercase representation. Many systems — including web APIs, databases, and URN formatting (RFC 4122 §3) — conventionally use lowercase UUIDs. `uuidStringLower` avoids the need to call `uuidString.lowercased()`, which allocates an intermediate `String`. + +### `span` property + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// A `Span` view of the UUID's 16 bytes. + public var span: Span { get } +} +``` + +This property provides zero-copy, bounds-checked access to the UUID's bytes without the need for `withUnsafeBytes` or tuple element access. The returned `Span` is lifetime-dependent on the UUID value. + +### Initializing from a `Span` + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// Creates a UUID by copying exactly 16 bytes from a `Span`. + public init(copying span: Span) +} +``` + +This initializer copies the bytes from a `Span` into a new UUID. The span must contain exactly 16 bytes; otherwise, the initializer traps. + +### Initializing from an `OutputSpan` + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// Creates a UUID by filling its 16 bytes using a closure that + /// writes into an `OutputSpan`. + /// + /// The closure must write exactly 16 bytes into the output span. + public init( + initializingWith initializer: (inout OutputSpan) throws(E) -> () + ) throws(E) +} +``` + +This initializer provides a safe, typed-throw-compatible way to construct a UUID from raw bytes without going through `uuid_t`: + +```swift +let uuid = UUID { output in + output.append(timestampBytes) + output.append(randomBytes) +} +``` + +The closure receives an `OutputSpan` backed by the UUID's 16-byte storage. If the closure writes fewer or more than 16 bytes, the initializer traps. If the closure throws, the error is propagated with its original type. + +### `UUID.Version` + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// The version of a UUID, as defined by RFC 9562. + public struct Version: Sendable, Hashable, Codable, RawRepresentable { + public let rawValue: UInt8 + public init(rawValue: UInt8) + + /// Version 1: Gregorian time-based UUID with node identifier. + public static var timeBased: Version { get } + + /// Version 3: Name-based UUID using MD5 hashing. + public static var nameBasedMD5: Version { get } + + /// Version 4: Random UUID. + public static var random: Version { get } + + /// Version 5: Name-based UUID using SHA-1 hashing. + public static var nameBasedSHA1: Version { get } + + /// Version 6: Reordered Gregorian time-based UUID. + public static var reorderedTimeBased: Version { get } + + /// Version 7: Unix Epoch time-based UUID with random bits. + public static var timeOrdered: Version { get } + + /// Version 8: Custom UUID with user-defined layout. + public static var custom: Version { get } + } +} +``` + +The version value is encoded in bits 48–51 of the UUID (the high nibble of byte 6), per RFC 9562. `Version` is a `RawRepresentable` struct rather than an enum, allowing new versions to be added without breaking source or binary compatibility. The well-known versions from RFC 9562 are provided as static properties. Versions 2 (DCE Security), 0 (nil UUID), and 15 (max UUID) do not have static properties but can be represented using `Version(rawValue:)` if needed. + +### `version` property + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// The version of this UUID, derived from the version bits + /// (bits 48–51) as defined by RFC 9562. + public var version: UUID.Version { + get + } +} +``` + +### Creating version 7 UUIDs + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// Creates a new UUID with RFC 9562 version 7 layout: a Unix + /// timestamp in milliseconds in the most significant 48 bits, + /// followed by random bits. The variant and version fields are + /// set per the RFC. + /// + /// Version 7 UUIDs sort in approximate chronological order + /// when compared using the standard `<` operator, making them + /// well-suited as database primary keys. UUIDs created within + /// the same millisecond are distinguished by random bits and + /// may not reflect exact creation order. + public static func timeOrdered() -> UUID + + /// Creates a new UUID with RFC 9562 version 7 layout using + /// the specified random number generator for the random bits. + /// + /// - Parameter generator: The random number generator to use + /// when creating the random portions of the UUID. + /// - Returns: A version 7 UUID. + public static func timeOrdered( + using generator: inout some RandomNumberGenerator + ) -> UUID +} +``` + +The resulting UUID contains a millisecond-precision Unix timestamp in bits 0–47, with version and variant fields set per RFC 9562. The remaining bits are filled using the system random number generator (for `timeOrdered()`) or the provided generator (for `timeOrdered(using:)`). The `timeOrdered()` convenience delegates to `timeOrdered(using:)` with a `SystemRandomNumberGenerator`. + +### Extracting the timestamp + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// For version 7 UUIDs, returns the `Date` encoded in the + /// most significant 48 bits. Returns `nil` for all other versions. + /// The returned date has millisecond precision, as specified + /// by RFC 9562. + public var timeOrderedTimestamp: Date? { + get + } +} +``` + +## Source compatibility + +This proposal is purely additive. The existing `UUID()` initializer continues to create version 4 random UUIDs. The `random(using:)` static method is unaffected. No existing behavior changes. + +UUIDs created by `timeOrdered()` are fully valid UUIDs and interoperate with all existing APIs that accept `UUID` or `NSUUID`, including `Codable`, `Comparable`, bridging, and string serialization. + +## Implications on adoption + +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility. + +## Future directions + +- **Version 5 (name-based SHA-1)**: A factory method like `UUID.nameBased(name:namespace:)` could be added in a future proposal for deterministic UUID generation. +- **Version 8 (custom)**: Could be exposed via an initializer that accepts the custom data bits while setting the version and variant fields automatically. For now, we do provide an initializer that allows for setting all of the bytes directly via `OutputSpan`. + +## Alternatives considered + +### Adding version as a parameter to `init()` + +Instead of `UUID.timeOrdered()`, we considered `UUID(version: .timeOrdered)`. However, different versions require different parameters — version 5 needs a name and namespace, version 8 needs custom data — so a single initializer would either need to accept many optional parameters or use an associated-value enum. Static factory methods are clearer and allow each version to have its own natural parameter list. + +### Using an `enum` for `Version` + +We considered making `Version` an `enum` with a `UInt8` raw value. However, a `struct` with `RawRepresentable` conformance allows new versions to be added in the future without breaking source or binary compatibility. Since the UUID version field is only 4 bits, the full space of 16 values is defined by the RFC, but using a struct is more consistent with Foundation's conventions for open sets of values (e.g., `NSNotificationName`, `RunLoop.Mode`) and avoids the need for an `unknown` case or optional return from the `version` property. + +### Supporting all UUID versions immediately + +We considered adding factory methods for all versions (1, 3, 5, 6, 7, 8), but the immediate need is version 7. Version 1 (time-based with MAC address) has privacy implications. Versions 3 and 5 require different parameters. Version 6 is a reordering of version 1 and shares its concerns. Version 8 is intentionally application-defined. Starting with version 7 keeps the proposal focused while the `Version` struct provides the foundation to add others incrementally. diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 568e12b889..3b13cd7b45 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -9,70 +9,149 @@ // //===----------------------------------------------------------------------===// -internal import _FoundationCShims // uuid.h - public typealias uuid_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) public typealias uuid_string_t = (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8) /// Represents UUID strings, which can be used to uniquely identify types, interfaces, and other items. @available(macOS 10.8, iOS 6.0, tvOS 9.0, watchOS 2.0, *) public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { - public private(set) var uuid: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + internal var _storage = InlineArray<16, UInt8>(repeating: 0) - /* Create a new UUID with RFC 4122 version 4 random bytes */ - public init() { - withUnsafeMutablePointer(to: &uuid) { - $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size) { - _foundation_uuid_generate_random($0) - } + /// The UUID bytes as a `uuid_t` tuple. + public var uuid: uuid_t { + get { + return unsafeBitCast(_storage, to: uuid_t.self) + } + set { + _storage = unsafeBitCast(newValue, to: InlineArray<16, UInt8>.self) } } - @inline(__always) - internal func withUUIDBytes(_ work: (UnsafeBufferPointer) throws -> R) rethrows -> R { - return try withExtendedLifetime(self) { - try withUnsafeBytes(of: uuid) { rawBuffer in - return try rawBuffer.withMemoryRebound(to: UInt8.self) { buffer in - return try work(buffer) - } - } - } + /* Create a new UUID with RFC 4122 version 4 random bytes */ + public init() { + var generator = SystemRandomNumberGenerator() + self = UUID.random(using: &generator) } /// Create a UUID from a string such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F". /// /// Returns nil for invalid strings. public init?(uuidString string: __shared String) { - let res = withUnsafeMutablePointer(to: &uuid) { - $0.withMemoryRebound(to: UInt8.self, capacity: 16) { - return _foundation_uuid_parse(string, $0) - } - } - if res != 0 { + let utf8 = string.utf8Span + guard utf8.count == 36 else { return nil } + + var charIdx = 0 + var byteIdx = 0 + while charIdx < 36 { + switch charIdx { + case 8, 13, 18, 23: + guard utf8.span[charIdx] == UInt8(ascii: "-") else { + return nil + } + charIdx += 1 + default: + // from CodableUtilities.swift + guard let b1 = utf8.span[charIdx].hexDigitValue else { + return nil + } + guard let b2 = utf8.span[charIdx + 1].hexDigitValue else { + return nil + } + _storage[byteIdx] = b1 << 4 | b2 + byteIdx += 1 + charIdx += 2 + } + } } /// Create a UUID from a `uuid_t`. public init(uuid: uuid_t) { - self.uuid = uuid + self._storage = unsafeBitCast(uuid, to: InlineArray<16, UInt8>.self) } - /// Returns a string created from the UUID, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" - public var uuidString: String { - var bytes: uuid_string_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - return withUUIDBytes { valBuffer in - withUnsafeMutablePointer(to: &bytes) { strPtr in - strPtr.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size) { str in - _foundation_uuid_unparse_upper(valBuffer.baseAddress!, str) - return String(cString: str) + /// Creates a UUID by copying exactly 16 bytes from a `Span`. + /// + /// - Precondition: `span.count` must be exactly 16. + @available(FoundationPreview 6.4, *) + public init(copying span: Span) { + precondition(span.count == 16, "UUID requires exactly 16 bytes, but \(span.count) were provided") + self.init() + for i in 0..<16 { + _storage[i] = span[i] + } + } + + /// Creates a UUID by filling its 16 bytes using a closure that + /// writes into an `OutputRawSpan`. + /// + /// The closure must write exactly 16 bytes into the output span. + /// + /// let uuid = UUID { output in + /// output.append(myTimestampBytes) + /// output.append(myRandomBytes) + /// } + @available(FoundationPreview 6.4, *) + public init( + initializingWith initializer: (inout OutputSpan) throws(E) -> () + ) throws(E) { + _storage = try InlineArray<16, UInt8>(initializingWith: { outputSpan throws(E) -> Void in + try initializer(&outputSpan) + let count = outputSpan.count + precondition(count == 16, "UUID requires exactly 16 bytes, but \(count) were provided") + }) + } + + // Hex lookup tables for UUID string formatting. + // Each byte is converted to two hex characters via table lookup. + private static let _upperHex: StaticString = "0123456789ABCDEF" + private static let _lowerHex: StaticString = "0123456789abcdef" + + /// Writes the UUID as a 36-character hex string into `buffer` + /// using the given hex digit lookup table. Returns 36. + private func _unparse( + into buffer: UnsafeMutableBufferPointer, + hexTable: StaticString + ) -> Int { + hexTable.withUTF8Buffer { hex in + var o = 0 + for i in 0..<16 { + // Insert '-' after bytes 4, 6, 8, 10 + switch i { + case 4, 6, 8, 10: + buffer[o] = UInt8(ascii: "-") + o &+= 1 + default: + break } + let byte = _storage[i] + buffer[o] = hex[Int(byte &>> 4)] + buffer[o &+ 1] = hex[Int(byte & 0xF)] + o &+= 2 } + assert(o == 36) + } + return 36 + } + + /// Returns a string created from the UUID, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + public var uuidString: String { + String(unsafeUninitializedCapacity: 36) { buffer in + _unparse(into: buffer, hexTable: UUID._upperHex) + } + } + + /// Returns a lowercase string created from the UUID, such as "e621e1f8-c36c-495a-93fc-0c247a3e6e5f" + @available(FoundationPreview 6.4, *) + public var uuidStringLower: String { + String(unsafeUninitializedCapacity: 36) { buffer in + _unparse(into: buffer, hexTable: UUID._lowerHex) } } public func hash(into hasher: inout Hasher) { - withUnsafeBytes(of: uuid) { buffer in + withUnsafeBytes(of: _storage) { buffer in hasher.combine(bytes: buffer) } } @@ -130,8 +209,9 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { } public static func ==(lhs: UUID, rhs: UUID) -> Bool { - withUnsafeBytes(of: lhs) { lhsPtr in - withUnsafeBytes(of: rhs) { rhsPtr in + // Implementation note: This operation is designed to avoid short-circuited early exits, so that comparison of any two UUID values is done in the same amount of time. + withUnsafeBytes(of: lhs._storage) { lhsPtr in + withUnsafeBytes(of: rhs._storage) { rhsPtr in let lhsTuple = lhsPtr.loadUnaligned(as: (UInt64, UInt64).self) let rhsTuple = rhsPtr.loadUnaligned(as: (UInt64, UInt64).self) return (lhsTuple.0 ^ rhsTuple.0) | (lhsTuple.1 ^ rhsTuple.1) == 0 @@ -169,17 +249,171 @@ extension UUID : Codable { } } +// MARK: - Nil and Max UUIDs + +@available(FoundationPreview 6.4, *) +extension UUID { + /// The nil UUID, where all bits are set to zero. + /// + /// As defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#section-5.9), + /// the nil UUID is a special form where all 128 bits are zero. + /// It can be used to represent the absence of a UUID value. + public static let `nil` = UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) + + /// The max UUID, where all bits are set to one. + /// + /// As defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#section-5.10), + /// the max UUID is a special form where all 128 bits are one. + /// It can be used as a sentinel value, for example to represent + /// "the largest possible UUID" in a sorted range. + public static let max = UUID(uuid: (0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF)) +} + +// MARK: - Span + +@available(FoundationPreview 6.4, *) +extension UUID { + /// A `Span` view of the UUID's 16 bytes. + public var span: Span { + @_lifetime(borrow self) + borrowing get { + _storage.span + } + } +} + +// MARK: - UUID Version + +@available(FoundationPreview 6.4, *) +extension UUID { + /// The version of a UUID, as defined by RFC 9562. + public struct Version: Sendable, Hashable, Codable, RawRepresentable { + public let rawValue: UInt8 + public init(rawValue: UInt8) { self.rawValue = rawValue } + + /// Version 1: Gregorian time-based UUID with node identifier. + public static var timeBased: Version { Version(rawValue: 1) } + + /// Version 3: Name-based UUID using MD5 hashing. + public static var nameBasedMD5: Version { Version(rawValue: 3) } + + /// Version 4: Random UUID. + public static var random: Version { Version(rawValue: 4) } + + /// Version 5: Name-based UUID using SHA-1 hashing. + public static var nameBasedSHA1: Version { Version(rawValue: 5) } + + /// Version 6: Reordered Gregorian time-based UUID. + public static var reorderedTimeBased: Version { Version(rawValue: 6) } + + /// Version 7: Unix Epoch time-based UUID with random bits. + public static var timeOrdered: Version { Version(rawValue: 7) } + + /// Version 8: Custom UUID with user-defined layout. + public static var custom: Version { Version(rawValue: 8) } + } + + /// The version of this UUID, derived from the version bits + /// (bits 48–51) as defined by RFC 9562. + public var version: UUID.Version { + Version(rawValue: _storage[6] >> 4) + } + + /// Creates a new UUID with RFC 9562 version 7 layout: a Unix + /// timestamp in milliseconds in the most significant 48 bits, + /// followed by random bits. The variant and version fields are + /// set per the RFC. + /// + /// Version 7 UUIDs sort in approximate chronological order + /// when compared using the standard `<` operator, making them + /// well-suited as database primary keys. UUIDs created within + /// the same millisecond are distinguished by random bits and + /// may not reflect exact creation order. + public static func timeOrdered() -> UUID { + var generator = SystemRandomNumberGenerator() + return timeOrdered(using: &generator) + } + + /// Creates a new UUID with RFC 9562 version 7 layout using + /// the specified random number generator for the random bits. + /// + /// The most significant 48 bits contain a millisecond-precision + /// Unix timestamp. The remaining bits (excluding version and + /// variant fields) are filled using `generator`. + /// + /// - Parameter generator: The random number generator to use + /// when creating the random portions of the UUID. + /// - Returns: A version 7 UUID. + public static func timeOrdered( + using generator: inout some RandomNumberGenerator + ) -> UUID { + let now = Date() + let rawMS = now.timeIntervalSince1970 * 1000.0 + // Clamp to the 48-bit unsigned range (0 ... 0xFFFF_FFFF_FFFF). + // Below 0 corresponds to dates before 1970-01-01. + // Above 0xFFFF_FFFF_FFFF corresponds to dates after approximately year 10889. + let ms = UInt64(clamping: Int64(Swift.max(0, Swift.min(rawMS, Double(0xFFFF_FFFF_FFFF))))) + + var first = UInt64.random(in: .min ... .max, using: &generator) + var second = UInt64.random(in: .min ... .max, using: &generator) + + // Set bits 0–47 to the millisecond timestamp + first = (first & 0x0000_0000_0000_FFFF) | (ms << 16) + + // Set the version to 7 (0111) in bits 48–51 + first &= 0xFFFF_FFFF_FFFF_0FFF + first |= 0x0000_0000_0000_7000 + + // Set the variant to '10' in bits 64–65 + second &= 0x3FFF_FFFF_FFFF_FFFF + second |= 0x8000_0000_0000_0000 + + return UUID { span in + // TODO: when OutputSpan has OutputRawSpan, we can append two UInt64 directly instead of breaking it down into bytes. + span.append(UInt8(truncatingIfNeeded: first >> 56)) + span.append(UInt8(truncatingIfNeeded: first >> 48)) + span.append(UInt8(truncatingIfNeeded: first >> 40)) + span.append(UInt8(truncatingIfNeeded: first >> 32)) + span.append(UInt8(truncatingIfNeeded: first >> 24)) + span.append(UInt8(truncatingIfNeeded: first >> 16)) + span.append(UInt8(truncatingIfNeeded: first >> 8)) + span.append(UInt8(truncatingIfNeeded: first)) + span.append(UInt8(truncatingIfNeeded: second >> 56)) + span.append(UInt8(truncatingIfNeeded: second >> 48)) + span.append(UInt8(truncatingIfNeeded: second >> 40)) + span.append(UInt8(truncatingIfNeeded: second >> 32)) + span.append(UInt8(truncatingIfNeeded: second >> 24)) + span.append(UInt8(truncatingIfNeeded: second >> 16)) + span.append(UInt8(truncatingIfNeeded: second >> 8)) + span.append(UInt8(truncatingIfNeeded: second)) + } + } + + /// For version 7 UUIDs, returns the `Date` encoded in the + /// most significant 48 bits. Returns `nil` for all other versions. + /// The returned date has millisecond precision, as specified + /// by RFC 9562. + public var timeOrderedTimestamp: Date? { + guard version == .timeOrdered else { return nil } + let ms: UInt64 = UInt64(_storage[0]) << 40 | UInt64(_storage[1]) << 32 + | UInt64(_storage[2]) << 24 | UInt64(_storage[3]) << 16 + | UInt64(_storage[4]) << 8 | UInt64(_storage[5]) + return Date(timeIntervalSince1970: Double(ms) / 1000.0) + } +} + @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) extension UUID : Comparable { @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) public static func < (lhs: UUID, rhs: UUID) -> Bool { - var leftUUID = lhs.uuid - var rightUUID = rhs.uuid + // Implementation note: This operation is designed to avoid short-circuited early exits, so that comparison of any two UUID values is done in the same amount of time. + var leftStorage = lhs._storage + var rightStorage = rhs._storage var result: Int = 0 var diff: Int = 0 - withUnsafeBytes(of: &leftUUID) { leftPtr in - withUnsafeBytes(of: &rightUUID) { rightPtr in - for offset in (0 ..< MemoryLayout.size).reversed() { + withUnsafeBytes(of: &leftStorage) { leftPtr in + withUnsafeBytes(of: &rightStorage) { rightPtr in + for offset in (0 ..< 16).reversed() { diff = Int(leftPtr.load(fromByteOffset: offset, as: UInt8.self)) - Int(rightPtr.load(fromByteOffset: offset, as: UInt8.self)) // Constant time, no branching equivalent of diff --git a/Sources/FoundationEssentials/UUID_Wrappers.swift b/Sources/FoundationEssentials/UUID_Wrappers.swift index 8dd2aa9d63..c7d5b6c44b 100644 --- a/Sources/FoundationEssentials/UUID_Wrappers.swift +++ b/Sources/FoundationEssentials/UUID_Wrappers.swift @@ -119,7 +119,7 @@ internal class __NSConcreteUUID : _NSUUIDBridge, @unchecked Sendable { } override func encode(with coder: NSCoder) { - _storage.withUUIDBytes { buffer in + _storage.span.withUnsafeBufferPointer { buffer in coder.encodeBytes(buffer.baseAddress, length: buffer.count, forKey: "NS.uuidbytes") } } @@ -148,7 +148,7 @@ internal class __NSConcreteUUID : _NSUUIDBridge, @unchecked Sendable { } override open func getBytes(_ bytes: UnsafeMutablePointer) { - _storage.withUUIDBytes { buffer in + _storage.span.withUnsafeBufferPointer { buffer in bytes.initialize(from: buffer.baseAddress!, count: buffer.count) } } diff --git a/Sources/_FoundationCShims/include/_FoundationCShims.h b/Sources/_FoundationCShims/include/_FoundationCShims.h index 33acfe14e9..dd8f17affd 100644 --- a/Sources/_FoundationCShims/include/_FoundationCShims.h +++ b/Sources/_FoundationCShims/include/_FoundationCShims.h @@ -22,7 +22,6 @@ #include "io_shims.h" #include "platform_shims.h" #include "filemanager_shims.h" -#include "uuid.h" #if FOUNDATION_FRAMEWORK && !TARGET_OS_EXCLAVEKIT #include "sandbox_shims.h" diff --git a/Sources/_FoundationCShims/include/uuid.h b/Sources/_FoundationCShims/include/uuid.h deleted file mode 100644 index d2e1c1560c..0000000000 --- a/Sources/_FoundationCShims/include/uuid.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2004 Apple Computer, Inc. All rights reserved. - * - * %Begin-Header% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, and the entire permission notice in its entirety, - * including the disclaimer of warranties. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ALL OF - * WHICH ARE HEREBY DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT - * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR - * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF NOT ADVISED OF THE POSSIBILITY OF SUCH - * DAMAGE. - * %End-Header% - */ - -#ifndef _CSHIMS_UUID_UUID_H -#define _CSHIMS_UUID_UUID_H - -#include "_CShimsTargetConditionals.h" -#include "_CShimsMacros.h" - -#if TARGET_OS_MAC -#include -#else -#include -typedef unsigned char __darwin_uuid_t[16]; -typedef char __darwin_uuid_string_t[37]; -#ifdef uuid_t -#undef uuid_t -#endif -typedef __darwin_uuid_t uuid_t; -typedef __darwin_uuid_string_t uuid_string_t; - -#define UUID_DEFINE(name,u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15) \ - static const uuid_t name __attribute__ ((unused)) = {u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15} -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -INTERNAL void _foundation_uuid_clear(uuid_t uu); - -INTERNAL int _foundation_uuid_compare(const uuid_t uu1, const uuid_t uu2); - -INTERNAL void _foundation_uuid_copy(uuid_t dst, const uuid_t src); - -INTERNAL void _foundation_uuid_generate(uuid_t out); -INTERNAL void _foundation_uuid_generate_random(uuid_t out); -INTERNAL void _foundation_uuid_generate_time(uuid_t out); - -INTERNAL int _foundation_uuid_is_null(const uuid_t uu); - -INTERNAL int _foundation_uuid_parse(const uuid_string_t in, uuid_t uu); - -INTERNAL void _foundation_uuid_unparse(const uuid_t uu, uuid_string_t out); -INTERNAL void _foundation_uuid_unparse_lower(const uuid_t uu, uuid_string_t out); -INTERNAL void _foundation_uuid_unparse_upper(const uuid_t uu, uuid_string_t out); - -#ifdef __cplusplus -} -#endif - -#endif /* _CSHIMS_UUID_UUID_H */ diff --git a/Sources/_FoundationCShims/uuid.c b/Sources/_FoundationCShims/uuid.c deleted file mode 100644 index 1b382cd1fe..0000000000 --- a/Sources/_FoundationCShims/uuid.c +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (c) 2004 Apple Computer, Inc. All rights reserved. - * - * %Begin-Header% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, and the entire permission notice in its entirety, - * including the disclaimer of warranties. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ALL OF - * WHICH ARE HEREBY DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT - * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR - * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF NOT ADVISED OF THE POSSIBILITY OF SUCH - * DAMAGE. - * %End-Header% - */ - -#include "include/uuid.h" - -#if __has_include() -#include -#endif - -#if TARGET_OS_MAC - -INTERNAL void _foundation_uuid_clear(uuid_t uu) { - uuid_clear(uu); -} - -INTERNAL int _foundation_uuid_compare(const uuid_t uu1, const uuid_t uu2) { - return uuid_compare(uu1, uu2); -} - -INTERNAL void _foundation_uuid_copy(uuid_t dst, const uuid_t src) { - uuid_copy(dst, src); -} - -INTERNAL void _foundation_uuid_generate(uuid_t out) { - uuid_generate(out); -} - -INTERNAL void _foundation_uuid_generate_random(uuid_t out) { - uuid_generate_random(out); -} - -INTERNAL void _foundation_uuid_generate_time(uuid_t out) { - uuid_generate_time(out); -} - -INTERNAL int _foundation_uuid_is_null(const uuid_t uu) { - return uuid_is_null(uu); -} - -INTERNAL int _foundation_uuid_parse(const uuid_string_t in, uuid_t uu) { - return uuid_parse(in, uu); -} - -INTERNAL void _foundation_uuid_unparse(const uuid_t uu, uuid_string_t out) { - uuid_unparse(uu, out); -} - -INTERNAL void _foundation_uuid_unparse_lower(const uuid_t uu, uuid_string_t out) { - uuid_unparse_lower(uu, out); -} - -INTERNAL void _foundation_uuid_unparse_upper(const uuid_t uu, uuid_string_t out) { - uuid_unparse_upper(uu, out); -} - -#else - -#include -#include -#include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) -#include -#elif defined(_WIN32) -#include -#define WIN32_LEAN_AND_MEAN -#include -#include -#endif -#include - -#if TARGET_OS_LINUX || TARGET_OS_BSD || TARGET_OS_WASI -#include - -static inline void nanotime(struct timespec *tv) { - clock_gettime(CLOCK_MONOTONIC, tv); -} - -#elif TARGET_OS_WINDOWS -#include - -static inline void nanotime(struct timespec *tv) { - FILETIME ftTime; - - GetSystemTimePreciseAsFileTime(&ftTime); - - uint64_t Value = (((uint64_t)ftTime.dwHighDateTime << 32) | ftTime.dwLowDateTime); - - tv->tv_sec = Value / 1000000000; - tv->tv_nsec = Value - (tv->tv_sec * 1000000000); -} -#endif - -#if TARGET_OS_WINDOWS -static inline void read_random(void *buffer, unsigned numBytes) { - BCryptGenRandom(NULL, buffer, numBytes, - BCRYPT_RNG_USE_ENTROPY_IN_BUFFER | BCRYPT_USE_SYSTEM_PREFERRED_RNG); -} -#elif TARGET_OS_WASI -#include - -static inline void read_random(void *buffer, unsigned numBytes) { - getentropy(buffer, numBytes); -} -#else -static inline void read_random(void *buffer, unsigned numBytes) { - int fd = open("/dev/urandom", O_RDONLY); - read(fd, buffer, numBytes); - close(fd); -} -#endif - -UUID_DEFINE(UUID_NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - -static void read_node(uint8_t *node) { -#if NETWORKING - struct ifnet *ifp; - struct ifaddr *ifa; - struct sockaddr_dl *sdl; - - ifnet_head_lock_shared(); - TAILQ_FOREACH(ifp, &ifnet_head, if_link) { - TAILQ_FOREACH(ifa, &ifp->if_addrhead, ifa_link) { - sdl = (struct sockaddr_dl *)ifa->ifa_addr; - if (sdl && sdl->sdl_family == AF_LINK && sdl->sdl_type == IFT_ETHER) { - memcpy(node, LLADDR(sdl), 6); - ifnet_head_done(); - return; - } - } - } - ifnet_head_done(); -#endif /* NETWORKING */ - - read_random(node, 6); - node[0] |= 0x01; -} - -static uint64_t read_time(void) { - struct timespec tv; - - nanotime(&tv); - - return (tv.tv_sec * 10000000ULL) + (tv.tv_nsec / 100ULL) + 0x01B21DD213814000ULL; -} - -void _foundation_uuid_clear(uuid_t uu) { - memset(uu, 0, sizeof(uuid_t)); -} - -int _foundation_uuid_compare(const uuid_t uu1, const uuid_t uu2) { - return memcmp(uu1, uu2, sizeof(uuid_t)); -} - -void _foundation_uuid_copy(uuid_t dst, const uuid_t src) { - memcpy(dst, src, sizeof(uuid_t)); -} - -void _foundation_uuid_generate_random(uuid_t out) { - read_random(out, sizeof(uuid_t)); - - out[6] = (out[6] & 0x0F) | 0x40; - out[8] = (out[8] & 0x3F) | 0x80; -} - -void _foundation_uuid_generate_time(uuid_t out) { - uint64_t time; - - read_node(&out[10]); - read_random(&out[8], 2); - - time = read_time(); - out[0] = (uint8_t)(time >> 24); - out[1] = (uint8_t)(time >> 16); - out[2] = (uint8_t)(time >> 8); - out[3] = (uint8_t)time; - out[4] = (uint8_t)(time >> 40); - out[5] = (uint8_t)(time >> 32); - out[6] = (uint8_t)(time >> 56); - out[7] = (uint8_t)(time >> 48); - - out[6] = (out[6] & 0x0F) | 0x10; - out[8] = (out[8] & 0x3F) | 0x80; -} - -void _foundation_uuid_generate(uuid_t out) -{ - _foundation_uuid_generate_random(out); -} - -int _foundation_uuid_is_null(const uuid_t uu) -{ - return !memcmp(uu, UUID_NULL, sizeof(uuid_t)); -} - -int _foundation_uuid_parse(const uuid_string_t in, uuid_t uu) -{ - int n = 0; - - sscanf(in, - "%2hhx%2hhx%2hhx%2hhx-" - "%2hhx%2hhx-" - "%2hhx%2hhx-" - "%2hhx%2hhx-" - "%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%n", - &uu[0], &uu[1], &uu[2], &uu[3], - &uu[4], &uu[5], - &uu[6], &uu[7], - &uu[8], &uu[9], - &uu[10], &uu[11], &uu[12], &uu[13], &uu[14], &uu[15], &n); - - return (n != 36 || in[n] != '\0' ? -1 : 0); -} - -void _foundation_uuid_unparse_lower(const uuid_t uu, uuid_string_t out) { - snprintf(out, - sizeof(uuid_string_t), - "%02x%02x%02x%02x-" - "%02x%02x-" - "%02x%02x-" - "%02x%02x-" - "%02x%02x%02x%02x%02x%02x", - uu[0], uu[1], uu[2], uu[3], - uu[4], uu[5], - uu[6], uu[7], - uu[8], uu[9], - uu[10], uu[11], uu[12], uu[13], uu[14], uu[15]); -} - -void _foundation_uuid_unparse_upper(const uuid_t uu, uuid_string_t out) { - snprintf(out, - sizeof(uuid_string_t), - "%02X%02X%02X%02X-" - "%02X%02X-" - "%02X%02X-" - "%02X%02X-" - "%02X%02X%02X%02X%02X%02X", - uu[0], uu[1], uu[2], uu[3], - uu[4], uu[5], - uu[6], uu[7], - uu[8], uu[9], - uu[10], uu[11], uu[12], uu[13], uu[14], uu[15]); -} - -void _foundation_uuid_unparse(const uuid_t uu, uuid_string_t out) { - _foundation_uuid_unparse_upper(uu, out); -} - -#endif - diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 784ab658d8..4626f43b56 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -47,6 +47,24 @@ private struct UUIDTests { #expect(uuid.uuidString == "E621E1F8-C36C-495A-93FC-0C247A3E6E5F", "The uuidString representation must be uppercase.") } + @available(FoundationPreview 6.4, *) + @Test func uuidStringLower() { + let uuid = UUID(uuid: (0xe6,0x21,0xe1,0xf8,0xc3,0x6c,0x49,0x5a,0x93,0xfc,0x0c,0x24,0x7a,0x3e,0x6e,0x5f)) + #expect(uuid.uuidStringLower == "e621e1f8-c36c-495a-93fc-0c247a3e6e5f") + } + + @available(FoundationPreview 6.4, *) + @Test func uuidStringLowerMatchesUpperCaseContent() { + let uuid = UUID() + #expect(uuid.uuidStringLower == uuid.uuidString.lowercased()) + } + + @available(FoundationPreview 6.4, *) + @Test func uuidStringLowerNilAndMax() { + #expect(UUID.nil.uuidStringLower == "00000000-0000-0000-0000-000000000000") + #expect(UUID.max.uuidStringLower == "ffffffff-ffff-ffff-ffff-ffffffffffff") + } + @Test func description() { let uuid = UUID() let description: String = uuid.description @@ -135,6 +153,240 @@ private struct UUIDTests { } } + @available(FoundationPreview 6.4, *) + @Test func nilUUID() { + let nilUUID = UUID.nil + #expect(nilUUID.uuidString == "00000000-0000-0000-0000-000000000000") + let s = nilUUID.span + for i in 0..<16 { + #expect(s[i] == 0) + } + } + + @available(FoundationPreview 6.4, *) + @Test func maxUUID() { + let maxUUID = UUID.max + #expect(maxUUID.uuidString == "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") + let s = maxUUID.span + for i in 0..<16 { + #expect(s[i] == 0xFF) + } + } + + @available(FoundationPreview 6.4, *) + @Test func nilAndMaxOrdering() { + let nilUUID = UUID.nil + let maxUUID = UUID.max + let randomUUID = UUID() + #expect(nilUUID < randomUUID) + #expect(randomUUID < maxUUID) + #expect(nilUUID < maxUUID) + } + + @available(FoundationPreview 6.4, *) + @Test func spanProperty() { + let uuid = UUID(uuid: (0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f)) + let s = uuid.span + #expect(s.count == 16) + #expect(s[0] == 0xe6) + #expect(s[1] == 0x21) + #expect(s[6] == 0x49) + #expect(s[15] == 0x5f) + } + + @available(FoundationPreview 6.4, *) + @Test func spanMatchesUUIDBytes() { + let uuid = UUID() + let s = uuid.span + let t = uuid.uuid + #expect(s[0] == t.0) + #expect(s[1] == t.1) + #expect(s[6] == t.6) + #expect(s[8] == t.8) + #expect(s[15] == t.15) + } + + @available(FoundationPreview 6.4, *) + @Test func initializingWithOutputSpan() { + let uuid = UUID { (output: inout OutputSpan) in + for i: UInt8 in 0..<16 { + output.append(i) + } + } + let s = uuid.span + for i: UInt8 in 0..<16 { + #expect(s[Int(i)] == i) + } + } + + @available(FoundationPreview 6.4, *) + @Test func initializingWithOutputSpanMatchesUUIDInit() { + let expected = UUID(uuid: (0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f)) + let bytes: [UInt8] = [0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f] + let uuid = UUID { (output: inout OutputSpan) in + for b in bytes { + output.append(b) + } + } + #expect(uuid == expected) + } + + @available(FoundationPreview 6.4, *) + @Test func initFromSpan() { + let bytes: [UInt8] = [0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f] + let span = bytes.span + let uuid = UUID(copying: span) + let expected = UUID(uuid: (0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f)) + #expect(uuid == expected) + } + + @available(FoundationPreview 6.4, *) + @Test func versionProperty() { + // UUID() creates v4 + let v4 = UUID() + #expect(v4.version == .random) + + // RFC 9562 Appendix A test vectors + // A.1: UUIDv1 + let v1 = UUID(uuidString: "C232AB00-9414-11EC-B3C8-9F6BDECED846")! + #expect(v1.version == .timeBased) + + // A.2: UUIDv3 + let v3 = UUID(uuidString: "5df41881-3aed-3515-88a7-2f4a814cf09e")! + #expect(v3.version == .nameBasedMD5) + + // A.3: UUIDv4 + let v4rfc = UUID(uuidString: "919108f7-52d1-4320-9bac-f847db4148a8")! + #expect(v4rfc.version == .random) + + // A.4: UUIDv5 + let v5 = UUID(uuidString: "2ed6657d-e927-568b-95e1-2665a8aea6a2")! + #expect(v5.version == .nameBasedSHA1) + + // A.5: UUIDv6 + let v6 = UUID(uuidString: "1EC9414C-232A-6B00-B3C8-9F6BDECED846")! + #expect(v6.version == .reorderedTimeBased) + + // A.6: UUIDv7 + let v7 = UUID(uuidString: "017F22E2-79B0-7CC3-98C4-DC0C0C07398F")! + #expect(v7.version == .timeOrdered) + + // B.1: UUIDv8 + let v8 = UUID(uuidString: "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0")! + #expect(v8.version == .custom) + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedVersionAndVariant() { + for _ in 0..<10000 { + let uuid = UUID.timeOrdered() + #expect(uuid.versionNumber == 0b0111) + #expect(uuid.varint == 0b10) + } + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedUsingGeneratorVersionAndVariant() { + var generator = SystemRandomNumberGenerator() + for _ in 0..<10000 { + let uuid = UUID.timeOrdered(using: &generator) + #expect(uuid.versionNumber == 0b0111) + #expect(uuid.varint == 0b10) + } + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedUsingGeneratorTimestamp() throws { + var generator = SystemRandomNumberGenerator() + let before = Date() + let uuid = UUID.timeOrdered(using: &generator) + let after = Date() + + let timestamp = try #require(uuid.timeOrderedTimestamp) + #expect(timestamp >= before.addingTimeInterval(-0.1)) + #expect(timestamp <= after.addingTimeInterval(0.1)) + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedUsingDeterministicGenerator() { + var gen1 = PCGRandomNumberGenerator(seed: 42) + var gen2 = PCGRandomNumberGenerator(seed: 42) + let uuid1 = UUID.timeOrdered(using: &gen1) + let uuid2 = UUID.timeOrdered(using: &gen2) + // Same seed at the same millisecond should produce the same random bits. + // The timestamps may differ slightly, so just check the random portions match. + // rand_a is the lower 12 bits of byte 6–7 (after version), rand_b is bytes 8–15 (after variant). + let s1 = uuid1.span + let s2 = uuid2.span + // Lower nibble of byte 6 + byte 7 = rand_a + #expect(s1[6] & 0x0F == s2[6] & 0x0F) + #expect(s1[7] == s2[7]) + // Lower 6 bits of byte 8 + bytes 9–15 = rand_b + #expect(s1[8] & 0x3F == s2[8] & 0x3F) + for i in 9..<16 { + #expect(s1[i] == s2[i]) + } + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedMonotonicity() async throws { + var previous = UUID.timeOrdered() + for _ in 0..<100 { + try await Task.sleep(for: .milliseconds(2)) + let current = UUID.timeOrdered() + #expect(previous < current) + previous = current + } + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedTimestamp() throws { + let before = Date() + let uuid = UUID.timeOrdered() + let after = Date() + + let timestamp = try #require(uuid.timeOrderedTimestamp) + #expect(timestamp >= before.addingTimeInterval(-0.1)) + #expect(timestamp <= after.addingTimeInterval(0.1)) + } + + // RFC 9562 Appendix A.6: UUIDv7 test vector with known timestamp + // Tuesday, February 22, 2022 2:22:22.00 PM GMT-05:00 = 1645557742000 ms + @available(FoundationPreview 6.4, *) + @Test func timeOrderedTimestampRFCVector() throws { + let v7 = UUID(uuidString: "017F22E2-79B0-7CC3-98C4-DC0C0C07398F")! + let timestamp = try #require(v7.timeOrderedTimestamp) + let expected = Date(timeIntervalSince1970: 1645557742.0) + #expect(timestamp == expected) + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedTimestampNilForV4() { + let uuid = UUID() + #expect(uuid.timeOrderedTimestamp == nil) + } + + @available(FoundationPreview 6.4, *) + @Test func versionRawValue() { + #expect(UUID.Version(rawValue: 1) == .timeBased) + #expect(UUID.Version(rawValue: 3) == .nameBasedMD5) + #expect(UUID.Version(rawValue: 4) == .random) + #expect(UUID.Version(rawValue: 5) == .nameBasedSHA1) + #expect(UUID.Version(rawValue: 6) == .reorderedTimeBased) + #expect(UUID.Version(rawValue: 7) == .timeOrdered) + #expect(UUID.Version(rawValue: 8) == .custom) + } + + @available(FoundationPreview 6.4, *) + @Test func versionForArbitraryBytes() { + for v: UInt8 in 0..<16 { + // Construct a UUID with the version nibble set to `v` + let byte6 = v << 4 + let uuid = UUID(uuid: (0, 0, 0, 0, 0, 0, byte6, 0, 0x80, 0, 0, 0, 0, 0, 0, 0)) + #expect(uuid.version.rawValue == v) + } + } + @available(FoundationPreview 6.3, *) @Test func deterministicRandomGeneration() { var generator = PCGRandomNumberGenerator(seed: 123456789) From a3e84b4e8cd5d7536b8c21d19a3feddd13357f6a Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Thu, 19 Mar 2026 09:03:00 -0700 Subject: [PATCH 02/15] Rename uuidStringLower to lowercasedUUIDString --- .../Essentials/BenchmarkEssentials.swift | 2 +- Proposals/NNNN-uuid-versions.md | 4 ++-- Sources/FoundationEssentials/UUID.swift | 2 +- Tests/FoundationEssentialsTests/UUIDTests.swift | 14 +++++++------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift index e9ef97d635..762eab723c 100644 --- a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift +++ b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift @@ -57,7 +57,7 @@ let benchmarks = { Benchmark("UUIDStringLower") { benchmark in let uuid = UUID() for _ in benchmark.scaledIterations { - blackHole(uuid.uuidStringLower) + blackHole(uuid.lowercasedUUIDString) } } } diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index 0fa162b45f..00715e06c9 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -78,11 +78,11 @@ The nil UUID (`00000000-0000-0000-0000-000000000000`) and max UUID (`FFFFFFFF-FF extension UUID { /// Returns a lowercase string created from the UUID, such as /// "e621e1f8-c36c-495a-93fc-0c247a3e6e5f". - public var uuidStringLower: String { get } + public var lowercasedUUIDString: String { get } } ``` -The existing `uuidString` property returns an uppercase representation. Many systems — including web APIs, databases, and URN formatting (RFC 4122 §3) — conventionally use lowercase UUIDs. `uuidStringLower` avoids the need to call `uuidString.lowercased()`, which allocates an intermediate `String`. +The existing `uuidString` property returns an uppercase representation. Many systems — including web APIs, databases, and URN formatting (RFC 4122 §3) — conventionally use lowercase UUIDs. `lowercasedUUIDString` avoids the need to call `uuidString.lowercased()`, which allocates an intermediate `String`. ### `span` property diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 3b13cd7b45..463fd2a408 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -144,7 +144,7 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { /// Returns a lowercase string created from the UUID, such as "e621e1f8-c36c-495a-93fc-0c247a3e6e5f" @available(FoundationPreview 6.4, *) - public var uuidStringLower: String { + public var lowercasedUUIDString: String { String(unsafeUninitializedCapacity: 36) { buffer in _unparse(into: buffer, hexTable: UUID._lowerHex) } diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 4626f43b56..8cb98524a9 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -48,21 +48,21 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func uuidStringLower() { + @Test func lowercasedUUIDString() { let uuid = UUID(uuid: (0xe6,0x21,0xe1,0xf8,0xc3,0x6c,0x49,0x5a,0x93,0xfc,0x0c,0x24,0x7a,0x3e,0x6e,0x5f)) - #expect(uuid.uuidStringLower == "e621e1f8-c36c-495a-93fc-0c247a3e6e5f") + #expect(uuid.lowercasedUUIDString == "e621e1f8-c36c-495a-93fc-0c247a3e6e5f") } @available(FoundationPreview 6.4, *) - @Test func uuidStringLowerMatchesUpperCaseContent() { + @Test func lowercasedUUIDStringMatchesUpperCaseContent() { let uuid = UUID() - #expect(uuid.uuidStringLower == uuid.uuidString.lowercased()) + #expect(uuid.lowercasedUUIDString == uuid.uuidString.lowercased()) } @available(FoundationPreview 6.4, *) - @Test func uuidStringLowerNilAndMax() { - #expect(UUID.nil.uuidStringLower == "00000000-0000-0000-0000-000000000000") - #expect(UUID.max.uuidStringLower == "ffffffff-ffff-ffff-ffff-ffffffffffff") + @Test func lowercasedUUIDStringNilAndMax() { + #expect(UUID.nil.lowercasedUUIDString == "00000000-0000-0000-0000-000000000000") + #expect(UUID.max.lowercasedUUIDString == "ffffffff-ffff-ffff-ffff-ffffffffffff") } @Test func description() { From ad291d5dd6d484a14031b544e5b27073f6185e00 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Thu, 19 Mar 2026 09:39:39 -0700 Subject: [PATCH 03/15] Fix CMake build --- Sources/_FoundationCShims/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/_FoundationCShims/CMakeLists.txt b/Sources/_FoundationCShims/CMakeLists.txt index d8e5d06a73..366663dfc4 100644 --- a/Sources/_FoundationCShims/CMakeLists.txt +++ b/Sources/_FoundationCShims/CMakeLists.txt @@ -14,8 +14,7 @@ add_library(_FoundationCShims STATIC platform_shims.c - string_shims.c - uuid.c) + string_shims.c) target_include_directories(_FoundationCShims PUBLIC include) From e5f5f440898fb67f89b44feb0cb341816c8a0b3b Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Thu, 19 Mar 2026 09:44:41 -0700 Subject: [PATCH 04/15] Add mutableSpan property, including proposal and tests --- Proposals/NNNN-uuid-versions.md | 7 ++++-- Sources/FoundationEssentials/UUID.swift | 8 +++++++ .../FoundationEssentialsTests/UUIDTests.swift | 24 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index 00715e06c9..7c6a9c6211 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -84,17 +84,20 @@ extension UUID { The existing `uuidString` property returns an uppercase representation. Many systems — including web APIs, databases, and URN formatting (RFC 4122 §3) — conventionally use lowercase UUIDs. `lowercasedUUIDString` avoids the need to call `uuidString.lowercased()`, which allocates an intermediate `String`. -### `span` property +### `span` and `mutableSpan` properties ```swift @available(FoundationPreview 6.4, *) extension UUID { /// A `Span` view of the UUID's 16 bytes. public var span: Span { get } + + /// A `MutableSpan` view of the UUID's 16 bytes. + public var mutableSpan: MutableSpan { mutating get } } ``` -This property provides zero-copy, bounds-checked access to the UUID's bytes without the need for `withUnsafeBytes` or tuple element access. The returned `Span` is lifetime-dependent on the UUID value. +These properties provide bounds-checked access to the UUID's bytes without the need for `withUnsafeBytes` or tuple element access. `span` provides read-only access; `mutableSpan` allows direct modification of the underlying bytes. Both are lifetime-dependent on the UUID value. ### Initializing from a `Span` diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 463fd2a408..8a1ee5ec75 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -280,6 +280,14 @@ extension UUID { _storage.span } } + + /// A `MutableSpan` view of the UUID's 16 bytes. + public var mutableSpan: MutableSpan { + @_lifetime(&self) + mutating get { + _storage.mutableSpan + } + } } // MARK: - UUID Version diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 8cb98524a9..31e0135bcb 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -206,6 +206,30 @@ private struct UUIDTests { #expect(s[15] == t.15) } + @available(FoundationPreview 6.4, *) + @Test func mutableSpan() { + var uuid = UUID.nil + var span = uuid.mutableSpan + span[0] = 0xAB + span[15] = 0xCD + #expect(uuid.span[0] == 0xAB) + #expect(uuid.span[15] == 0xCD) + // Other bytes remain zero + for i in 1..<15 { + #expect(uuid.span[i] == 0) + } + } + + @available(FoundationPreview 6.4, *) + @Test func mutableSpanModifiesUUID() { + var uuid = UUID(uuid: (0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f)) + // Overwrite version nibble to v7 + let previousValue = uuid.span[6] + var span = uuid.mutableSpan + span[6] = (previousValue & 0x0F) | 0x70 + #expect(uuid.version == .timeOrdered) + } + @available(FoundationPreview 6.4, *) @Test func initializingWithOutputSpan() { let uuid = UUID { (output: inout OutputSpan) in From 0c56b2c55923420d68117c4d778f3c6a39648653 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Thu, 19 Mar 2026 09:48:19 -0700 Subject: [PATCH 05/15] Reference this PR and pitch thread --- Proposals/NNNN-uuid-versions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index 7c6a9c6211..85eea2a9cb 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -4,8 +4,8 @@ * Authors: [Tony Parker](https://github.com/parkera) * Review Manager: TBD * Status: **Awaiting review** -* Implementation: [swiftlang/swift-foundation#NNNNN](https://github.com/swiftlang/swift-foundation/pull/NNNNN) -* Review: ([pitch](https://forums.swift.org/...)) +* Implementation: [swiftlang/swift-foundation#NNNNN](https://github.com/swiftlang/swift-foundation/pull/1836) +* Review: ([pitch](https://forums.swift.org/t/pitch-uuid-v7-other-improvements/85427)) ## Introduction From b3980b4f6ac4bf2e7308c9bd7c373d6b442fac34 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Thu, 19 Mar 2026 09:54:29 -0700 Subject: [PATCH 06/15] Add option to specify Date alongside the random number generator when producing v7 UUID --- Proposals/NNNN-uuid-versions.md | 7 ++- Sources/FoundationEssentials/UUID.swift | 5 ++- .../FoundationEssentialsTests/UUIDTests.swift | 45 ++++++++++++------- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index 85eea2a9cb..af8b58c36f 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -208,14 +208,17 @@ extension UUID { /// /// - Parameter generator: The random number generator to use /// when creating the random portions of the UUID. + /// - Parameter date: The date to encode in the timestamp field. + /// If `nil`, the current date is used. /// - Returns: A version 7 UUID. public static func timeOrdered( - using generator: inout some RandomNumberGenerator + using generator: inout some RandomNumberGenerator, + at date: Date? = nil ) -> UUID } ``` -The resulting UUID contains a millisecond-precision Unix timestamp in bits 0–47, with version and variant fields set per RFC 9562. The remaining bits are filled using the system random number generator (for `timeOrdered()`) or the provided generator (for `timeOrdered(using:)`). The `timeOrdered()` convenience delegates to `timeOrdered(using:)` with a `SystemRandomNumberGenerator`. +The resulting UUID contains a millisecond-precision Unix timestamp in bits 0–47, with version and variant fields set per RFC 9562. The remaining bits are filled using the system random number generator (for `timeOrdered()`) or the provided generator (for `timeOrdered(using:at:)`). The `timeOrdered()` convenience delegates to `timeOrdered(using:)` with a `SystemRandomNumberGenerator`. The optional `date` parameter allows embedding a specific timestamp rather than the current time. ### Extracting the timestamp diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 8a1ee5ec75..a33e7a4d44 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -353,9 +353,10 @@ extension UUID { /// when creating the random portions of the UUID. /// - Returns: A version 7 UUID. public static func timeOrdered( - using generator: inout some RandomNumberGenerator + using generator: inout some RandomNumberGenerator, + at date: Date? = nil ) -> UUID { - let now = Date() + let now = date ?? Date.now let rawMS = now.timeIntervalSince1970 * 1000.0 // Clamp to the 48-bit unsigned range (0 ... 0xFFFF_FFFF_FFFF). // Below 0 corresponds to dates before 1970-01-01. diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 31e0135bcb..3d57cb5344 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -333,23 +333,38 @@ private struct UUIDTests { @available(FoundationPreview 6.4, *) @Test func timeOrderedUsingDeterministicGenerator() { + let fixedDate = Date(timeIntervalSince1970: 1645557742.0) // RFC 9562 A.6 timestamp var gen1 = PCGRandomNumberGenerator(seed: 42) var gen2 = PCGRandomNumberGenerator(seed: 42) - let uuid1 = UUID.timeOrdered(using: &gen1) - let uuid2 = UUID.timeOrdered(using: &gen2) - // Same seed at the same millisecond should produce the same random bits. - // The timestamps may differ slightly, so just check the random portions match. - // rand_a is the lower 12 bits of byte 6–7 (after version), rand_b is bytes 8–15 (after variant). - let s1 = uuid1.span - let s2 = uuid2.span - // Lower nibble of byte 6 + byte 7 = rand_a - #expect(s1[6] & 0x0F == s2[6] & 0x0F) - #expect(s1[7] == s2[7]) - // Lower 6 bits of byte 8 + bytes 9–15 = rand_b - #expect(s1[8] & 0x3F == s2[8] & 0x3F) - for i in 9..<16 { - #expect(s1[i] == s2[i]) - } + let uuid1 = UUID.timeOrdered(using: &gen1, at: fixedDate) + let uuid2 = UUID.timeOrdered(using: &gen2, at: fixedDate) + // Same seed and same date produces identical UUIDs + #expect(uuid1 == uuid2) + // Verify the timestamp round-trips + #expect(uuid1.timeOrderedTimestamp == fixedDate) + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedDifferentSeedsSameDate() { + let fixedDate = Date(timeIntervalSince1970: 1645557742.0) + var gen1 = PCGRandomNumberGenerator(seed: 42) + var gen2 = PCGRandomNumberGenerator(seed: 99) + let uuid1 = UUID.timeOrdered(using: &gen1, at: fixedDate) + let uuid2 = UUID.timeOrdered(using: &gen2, at: fixedDate) + // Same date but different seeds produces different UUIDs + #expect(uuid1 != uuid2) + // Both should still have the same timestamp + #expect(uuid1.timeOrderedTimestamp == uuid2.timeOrderedTimestamp) + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedAtSpecificDate() throws { + let date = Date(timeIntervalSince1970: 1000.0) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.timeOrdered(using: &generator, at: date) + let timestamp = try #require(uuid.timeOrderedTimestamp) + #expect(timestamp == date) + #expect(uuid.version == .timeOrdered) } @available(FoundationPreview 6.4, *) From 95d5fbbae7858ceb2175bc4d0c27f878b291a152 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Thu, 19 Mar 2026 14:06:52 -0700 Subject: [PATCH 07/15] Rename nil UUID to min UUID --- Proposals/NNNN-uuid-versions.md | 18 ++++++++--------- Sources/FoundationEssentials/UUID.swift | 4 ++-- .../FoundationEssentialsTests/UUIDTests.swift | 20 +++++++++---------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index af8b58c36f..1b0df00219 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -11,7 +11,7 @@ Foundation's `UUID` type currently generates only version 4 (random) UUIDs. [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562) defines several UUID versions, each suited to different use cases. This proposal adds support for creating UUIDs of version 7 (time-ordered) which has become widely adopted for database keys and distributed systems due to its monotonically increasing, sortable nature. -In addition, `UUID` is in need of a few more additions for modern usage, including support for lowercase strings, access to the bytes using `Span`, and accessors for the commonly used `nil` and `max` sentinel values. +In addition, `UUID` is in need of a few more additions for modern usage, including support for lowercase strings, access to the bytes using `Span`, and accessors for the commonly used `min` and `max` sentinel values. ## Motivation @@ -21,7 +21,7 @@ Today, developers who need time-ordered UUIDs usually construct the bytes manual ## Proposed solution -Add a `UUID.Version` struct representing the well-known UUID versions from RFC 9562, a `version` property on `UUID` for introspection, a static factory method for creating version 7 UUIDs, and convenience properties for the nil and max UUIDs. +Add a `UUID.Version` struct representing the well-known UUID versions from RFC 9562, a `version` property on `UUID` for introspection, a static factory method for creating version 7 UUIDs, and convenience properties for the nil (which we name `min` to avoid confusion in Swift) and max UUIDs. ```swift // Create a time-ordered UUID @@ -41,8 +41,8 @@ default: let randomID = UUID() assert(randomID.version == .random) -// Nil and max UUIDs for sentinel values -let nilID = UUID.nil // 00000000-0000-0000-0000-000000000000 +// Min and max UUIDs for sentinel values +let minID = UUID.min // 00000000-0000-0000-0000-000000000000 let maxID = UUID.max // FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF // Access the raw bytes without copying @@ -52,15 +52,15 @@ let span: Span = uuid.span // 16-element typed span ## Detailed design -### Nil and Max UUIDs +### Min and Max UUIDs ```swift @available(FoundationPreview 6.4, *) extension UUID { - /// The nil UUID, where all 128 bits are set to zero, as defined by + /// The minimum UUID, where all 128 bits are set to zero, as defined by /// RFC 9562 Section 5.9. Can be used to represent the absence of a /// UUID value. - public static let `nil`: UUID + public static let min: UUID /// The max UUID, where all 128 bits are set to one, as defined by /// RFC 9562 Section 5.10. Can be used as a sentinel value, for example @@ -69,7 +69,7 @@ extension UUID { } ``` -The nil UUID (`00000000-0000-0000-0000-000000000000`) and max UUID (`FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF`) are special forms defined by RFC 9562. They are useful as sentinel values — for example, representing "no UUID" or defining the bounds of a UUID range. Note that neither the nil UUID nor the max UUID has a meaningful version or variant field; the `version` property returns `Version(rawValue: 0)` and `Version(rawValue: 15)` respectively. +The min UUID (`00000000-0000-0000-0000-000000000000`) and max UUID (`FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF`) are special forms defined by RFC 9562. They are useful as sentinel values — for example, representing "no UUID" or defining the bounds of a UUID range. Note that neither the min UUID nor the max UUID has a meaningful version or variant field; the `version` property returns `Version(rawValue: 0)` and `Version(rawValue: 15)` respectively. ### Lowercase string representation @@ -171,7 +171,7 @@ extension UUID { } ``` -The version value is encoded in bits 48–51 of the UUID (the high nibble of byte 6), per RFC 9562. `Version` is a `RawRepresentable` struct rather than an enum, allowing new versions to be added without breaking source or binary compatibility. The well-known versions from RFC 9562 are provided as static properties. Versions 2 (DCE Security), 0 (nil UUID), and 15 (max UUID) do not have static properties but can be represented using `Version(rawValue:)` if needed. +The version value is encoded in bits 48–51 of the UUID (the high nibble of byte 6), per RFC 9562. `Version` is a `RawRepresentable` struct rather than an enum, allowing new versions to be added without breaking source or binary compatibility. The well-known versions from RFC 9562 are provided as static properties. Versions 2 (DCE Security), 0 (min UUID), and 15 (max UUID) do not have static properties but can be represented using `Version(rawValue:)` if needed. ### `version` property diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index a33e7a4d44..c8792e266e 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -253,12 +253,12 @@ extension UUID : Codable { @available(FoundationPreview 6.4, *) extension UUID { - /// The nil UUID, where all bits are set to zero. + /// The `nil` (or minimum) UUID, where all bits are set to zero. /// /// As defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#section-5.9), /// the nil UUID is a special form where all 128 bits are zero. /// It can be used to represent the absence of a UUID value. - public static let `nil` = UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) + public static let min = UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) /// The max UUID, where all bits are set to one. /// diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 3d57cb5344..c2d9554ad8 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -61,7 +61,7 @@ private struct UUIDTests { @available(FoundationPreview 6.4, *) @Test func lowercasedUUIDStringNilAndMax() { - #expect(UUID.nil.lowercasedUUIDString == "00000000-0000-0000-0000-000000000000") + #expect(UUID.min.lowercasedUUIDString == "00000000-0000-0000-0000-000000000000") #expect(UUID.max.lowercasedUUIDString == "ffffffff-ffff-ffff-ffff-ffffffffffff") } @@ -154,10 +154,10 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func nilUUID() { - let nilUUID = UUID.nil - #expect(nilUUID.uuidString == "00000000-0000-0000-0000-000000000000") - let s = nilUUID.span + @Test func minUUID() { + let minUUID = UUID.min + #expect(minUUID.uuidString == "00000000-0000-0000-0000-000000000000") + let s = minUUID.span for i in 0..<16 { #expect(s[i] == 0) } @@ -174,13 +174,13 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func nilAndMaxOrdering() { - let nilUUID = UUID.nil + @Test func minAndMaxOrdering() { + let minUUID = UUID.min let maxUUID = UUID.max let randomUUID = UUID() - #expect(nilUUID < randomUUID) + #expect(minUUID < randomUUID) #expect(randomUUID < maxUUID) - #expect(nilUUID < maxUUID) + #expect(minUUID < maxUUID) } @available(FoundationPreview 6.4, *) @@ -208,7 +208,7 @@ private struct UUIDTests { @available(FoundationPreview 6.4, *) @Test func mutableSpan() { - var uuid = UUID.nil + var uuid = UUID.min var span = uuid.mutableSpan span[0] = 0xAB span[15] = 0xCD From d893e42b8dfe8b4e10e566059b2744c846d21f38 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Fri, 20 Mar 2026 10:51:45 -0700 Subject: [PATCH 08/15] Switch (mostly) from Date to Duration, fill the 12 bits with more precision --- Proposals/NNNN-uuid-versions.md | 10 +- Sources/FoundationEssentials/UUID.swift | 110 +++++++++++++++--- .../FoundationEssentialsTests/UUIDTests.swift | 37 ++++-- 3 files changed, 122 insertions(+), 35 deletions(-) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index 1b0df00219..9b7f51a683 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -208,17 +208,19 @@ extension UUID { /// /// - Parameter generator: The random number generator to use /// when creating the random portions of the UUID. - /// - Parameter date: The date to encode in the timestamp field. - /// If `nil`, the current date is used. + /// - Parameter timeSince1970: The time since the Unix epoch to + /// encode in the timestamp field. If `nil`, the current time + /// is used. `Duration` provides sub-millisecond precision + /// without floating-point loss. /// - Returns: A version 7 UUID. public static func timeOrdered( using generator: inout some RandomNumberGenerator, - at date: Date? = nil + timeSince1970: Duration? = nil ) -> UUID } ``` -The resulting UUID contains a millisecond-precision Unix timestamp in bits 0–47, with version and variant fields set per RFC 9562. The remaining bits are filled using the system random number generator (for `timeOrdered()`) or the provided generator (for `timeOrdered(using:at:)`). The `timeOrdered()` convenience delegates to `timeOrdered(using:)` with a `SystemRandomNumberGenerator`. The optional `date` parameter allows embedding a specific timestamp rather than the current time. +The resulting UUID contains a millisecond-precision Unix timestamp in bits 0–47, with version and variant fields set per RFC 9562. The 12-bit `rand_a` field (bits 52–63) encodes sub-millisecond timestamp precision per RFC 9562 Section 6.2, Method 3: the fractional millisecond is scaled to 12 bits using integer arithmetic on `Duration`'s attosecond components, avoiding any floating-point precision loss. The remaining 62 bits (`rand_b`) are filled using the system random number generator (for `timeOrdered()`) or the provided generator (for `timeOrdered(using:timeSince1970:)`). The `timeOrdered()` convenience delegates to `timeOrdered(using:)` with a `SystemRandomNumberGenerator`. The optional `timeSince1970` parameter accepts a `Duration` representing the time since the Unix epoch, allowing callers to embed a specific timestamp. ### Extracting the timestamp diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index c8792e266e..8dbe4f1642 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -9,6 +9,20 @@ // //===----------------------------------------------------------------------===// +#if canImport(Darwin) +import Darwin +#elseif canImport(Bionic) +@preconcurrency import Bionic +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(WinSDK) +import WinSDK +#elseif os(WASI) +@preconcurrency import WASILibc +#endif + public typealias uuid_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) public typealias uuid_string_t = (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8) @@ -346,33 +360,35 @@ extension UUID { /// the specified random number generator for the random bits. /// /// The most significant 48 bits contain a millisecond-precision - /// Unix timestamp. The remaining bits (excluding version and - /// variant fields) are filled using `generator`. + /// Unix timestamp. The 12 bits following the version field + /// (`rand_a`) encode sub-millisecond timestamp precision per + /// RFC 9562 Section 6.2, Method 3. The remaining 62 bits + /// (`rand_b`, after the variant field) are filled using `generator`. /// /// - Parameter generator: The random number generator to use /// when creating the random portions of the UUID. + /// - Parameter timeSince1970: The time since the Unix epoch to + /// encode in the timestamp field. If `nil`, the current time + /// is used. `Duration` provides sub-millisecond precision + /// without floating-point loss. /// - Returns: A version 7 UUID. public static func timeOrdered( using generator: inout some RandomNumberGenerator, - at date: Date? = nil + timeSince1970: Duration? = nil ) -> UUID { - let now = date ?? Date.now - let rawMS = now.timeIntervalSince1970 * 1000.0 - // Clamp to the 48-bit unsigned range (0 ... 0xFFFF_FFFF_FFFF). - // Below 0 corresponds to dates before 1970-01-01. - // Above 0xFFFF_FFFF_FFFF corresponds to dates after approximately year 10889. - let ms = UInt64(clamping: Int64(Swift.max(0, Swift.min(rawMS, Double(0xFFFF_FFFF_FFFF))))) - - var first = UInt64.random(in: .min ... .max, using: &generator) + let elapsed = timeSince1970 ?? Duration.durationSince1970 + let (ms, subMS) = elapsed._uuidTimestampComponents + + var first: UInt64 = 0 + // Bits 0–47: millisecond timestamp + first |= ms << 16 + // Bits 48–51: version 7 (0111) + first |= 0x7000 + // Bits 52–63: sub-millisecond precision (12 bits) + first |= UInt64(subMS) + + // Bits 64–127: variant + random var second = UInt64.random(in: .min ... .max, using: &generator) - - // Set bits 0–47 to the millisecond timestamp - first = (first & 0x0000_0000_0000_FFFF) | (ms << 16) - - // Set the version to 7 (0111) in bits 48–51 - first &= 0xFFFF_FFFF_FFFF_0FFF - first |= 0x0000_0000_0000_7000 - // Set the variant to '10' in bits 64–65 second &= 0x3FFF_FFFF_FFFF_FFFF second |= 0x8000_0000_0000_0000 @@ -409,6 +425,62 @@ extension UUID { | UInt64(_storage[4]) << 8 | UInt64(_storage[5]) return Date(timeIntervalSince1970: Double(ms) / 1000.0) } + + // MARK: - Private time helpers +} + +extension Duration { + /// Attoseconds per millisecond (10^15). + private static let _attosPerMS: Int64 = 1_000_000_000_000_000 + + /// The current wall clock time as a `Duration` since the Unix epoch, + /// using the highest precision time source available on the platform. + fileprivate static var durationSince1970: Duration { +#if canImport(WinSDK) + // FILETIME is 100-nanosecond intervals since January 1, 1601 (UTC). + // Subtract the 1601-to-1970 offset to get Unix epoch time. + var ft = FILETIME() + GetSystemTimePreciseAsFileTime(&ft) + var li = ULARGE_INTEGER() + li.LowPart = ft.dwLowDateTime + li.HighPart = ft.dwHighDateTime + // 100-ns ticks from 1601 to 1970 + let epochOffset: UInt64 = 116_444_736_000_000_000 + let ticks = li.QuadPart - epochOffset + let seconds = Int64(ticks / 10_000_000) + // Each tick is 100ns = 100_000_000_000 attoseconds + let remainingTicks = Int64(ticks % 10_000_000) + return Duration.seconds(seconds) + Duration(secondsComponent: 0, attosecondsComponent: remainingTicks * 100_000_000_000) +#else + var ts = timespec() + clock_gettime(CLOCK_REALTIME, &ts) + return Duration.seconds(ts.tv_sec) + Duration.nanoseconds(ts.tv_nsec) +#endif + } + + /// Extracts the 48-bit millisecond timestamp and 12-bit + /// sub-millisecond fraction from this duration (interpreted as + /// time since Unix epoch), using pure integer arithmetic. + /// + /// Returns `(ms, subMS)` where `ms` is clamped to 48 bits and + /// `subMS` is 0–4095. + fileprivate var _uuidTimestampComponents: (ms: UInt64, subMS: UInt16) { + let (secs, attos) = self.components + + // Total milliseconds = seconds * 1000 + attoseconds / attosPerMS + let totalMS = Int64(secs) * 1000 + attos / Self._attosPerMS + + // Clamp to the 48-bit unsigned range (0 ... 0xFFFF_FFFF_FFFF) + let ms = UInt64(clamping: Swift.max(0, totalMS)) + & 0xFFFF_FFFF_FFFF + + // Sub-millisecond fraction: remaining attoseconds after + // removing whole milliseconds, scaled to 12 bits. + let remainingAttos = attos % Self._attosPerMS + let subMS = UInt16((remainingAttos * 4096) / Self._attosPerMS) + + return (ms, subMS) + } } @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index c2d9554ad8..9ceef278a8 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -333,25 +333,25 @@ private struct UUIDTests { @available(FoundationPreview 6.4, *) @Test func timeOrderedUsingDeterministicGenerator() { - let fixedDate = Date(timeIntervalSince1970: 1645557742.0) // RFC 9562 A.6 timestamp + let fixedTime = Duration.seconds(1645557742) // RFC 9562 A.6 timestamp var gen1 = PCGRandomNumberGenerator(seed: 42) var gen2 = PCGRandomNumberGenerator(seed: 42) - let uuid1 = UUID.timeOrdered(using: &gen1, at: fixedDate) - let uuid2 = UUID.timeOrdered(using: &gen2, at: fixedDate) - // Same seed and same date produces identical UUIDs + let uuid1 = UUID.timeOrdered(using: &gen1, timeSince1970: fixedTime) + let uuid2 = UUID.timeOrdered(using: &gen2, timeSince1970: fixedTime) + // Same seed and same time produces identical UUIDs #expect(uuid1 == uuid2) // Verify the timestamp round-trips - #expect(uuid1.timeOrderedTimestamp == fixedDate) + #expect(uuid1.timeOrderedTimestamp == Date(timeIntervalSince1970: 1645557742.0)) } @available(FoundationPreview 6.4, *) @Test func timeOrderedDifferentSeedsSameDate() { - let fixedDate = Date(timeIntervalSince1970: 1645557742.0) + let fixedTime = Duration.seconds(1645557742) var gen1 = PCGRandomNumberGenerator(seed: 42) var gen2 = PCGRandomNumberGenerator(seed: 99) - let uuid1 = UUID.timeOrdered(using: &gen1, at: fixedDate) - let uuid2 = UUID.timeOrdered(using: &gen2, at: fixedDate) - // Same date but different seeds produces different UUIDs + let uuid1 = UUID.timeOrdered(using: &gen1, timeSince1970: fixedTime) + let uuid2 = UUID.timeOrdered(using: &gen2, timeSince1970: fixedTime) + // Same time but different seeds produces different UUIDs #expect(uuid1 != uuid2) // Both should still have the same timestamp #expect(uuid1.timeOrderedTimestamp == uuid2.timeOrderedTimestamp) @@ -359,14 +359,27 @@ private struct UUIDTests { @available(FoundationPreview 6.4, *) @Test func timeOrderedAtSpecificDate() throws { - let date = Date(timeIntervalSince1970: 1000.0) + let time = Duration.seconds(1000) var generator = SystemRandomNumberGenerator() - let uuid = UUID.timeOrdered(using: &generator, at: date) + let uuid = UUID.timeOrdered(using: &generator, timeSince1970: time) let timestamp = try #require(uuid.timeOrderedTimestamp) - #expect(timestamp == date) + #expect(timestamp == Date(timeIntervalSince1970: 1000.0)) #expect(uuid.version == .timeOrdered) } + @available(FoundationPreview 6.4, *) + @Test func timeOrderedSubMillisecondPrecision() { + // RFC 9562 Section 6.2 Method 3: rand_a encodes sub-ms precision. + // 456_789 nanoseconds = 456_789_000_000_000 attoseconds + // rand_a = (456_789_000_000_000 * 4096) / 1_000_000_000_000_000 = 1871 + let time = Duration.seconds(1000) + Duration.nanoseconds(123_456_789) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.timeOrdered(using: &generator, timeSince1970: time) + // rand_a is the lower nibble of byte 6 and all of byte 7 + let randA = (UInt16(uuid.span[6]) & 0x0F) << 8 | UInt16(uuid.span[7]) + #expect(randA == 1871) + } + @available(FoundationPreview 6.4, *) @Test func timeOrderedMonotonicity() async throws { var previous = UUID.timeOrdered() From 21774b58bb01284a5ecba0159b84a84345fb1082 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Fri, 20 Mar 2026 12:51:50 -0700 Subject: [PATCH 09/15] Add offset, promise of monotonic behavior --- Proposals/NNNN-uuid-versions.md | 43 +++--- Sources/FoundationEssentials/UUID.swift | 127 ++++++++++-------- .../FoundationEssentialsTests/UUIDTests.swift | 79 ++++++++--- 3 files changed, 147 insertions(+), 102 deletions(-) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index 9b7f51a683..463aaa29b4 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -126,7 +126,7 @@ extension UUID { } ``` -This initializer provides a safe, typed-throw-compatible way to construct a UUID from raw bytes without going through `uuid_t`: +This initializer provides a safe, typed-throw-compatible way to construct a UUID from bytes without going through `uuid_t`: ```swift let uuid = UUID { output in @@ -191,46 +191,41 @@ extension UUID { ```swift @available(FoundationPreview 6.4, *) extension UUID { - /// Creates a new UUID with RFC 9562 version 7 layout: a Unix - /// timestamp in milliseconds in the most significant 48 bits, - /// followed by random bits. The variant and version fields are - /// set per the RFC. + /// Creates a new UUID with RFC 9562 version 7 layout: a Unix timestamp in milliseconds in the most significant 48 bits, followed by random bits. The variant and version fields are set per the RFC. /// - /// Version 7 UUIDs sort in approximate chronological order - /// when compared using the standard `<` operator, making them - /// well-suited as database primary keys. UUIDs created within - /// the same millisecond are distinguished by random bits and - /// may not reflect exact creation order. + /// Version 7 UUIDs sort in chronological order when compared using the standard `<` operator, making them well-suited as database primary keys. UUIDs generated within the same process are guaranteed to be monotonically increasing. public static func timeOrdered() -> UUID - /// Creates a new UUID with RFC 9562 version 7 layout using - /// the specified random number generator for the random bits. + /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits. /// - /// - Parameter generator: The random number generator to use - /// when creating the random portions of the UUID. - /// - Parameter timeSince1970: The time since the Unix epoch to - /// encode in the timestamp field. If `nil`, the current time - /// is used. `Duration` provides sub-millisecond precision - /// without floating-point loss. + /// When called without an `at` argument, the timestamp portion is guaranteed to be monotonically increasing within the current process, even under high-frequency generation or clock adjustments. + /// + /// - Parameter generator: The random number generator to use when creating the random portions of the UUID. + /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. + /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. /// - Returns: A version 7 UUID. public static func timeOrdered( using generator: inout some RandomNumberGenerator, - timeSince1970: Duration? = nil + at date: Date? = nil, + offset: Duration = .zero ) -> UUID } ``` -The resulting UUID contains a millisecond-precision Unix timestamp in bits 0–47, with version and variant fields set per RFC 9562. The 12-bit `rand_a` field (bits 52–63) encodes sub-millisecond timestamp precision per RFC 9562 Section 6.2, Method 3: the fractional millisecond is scaled to 12 bits using integer arithmetic on `Duration`'s attosecond components, avoiding any floating-point precision loss. The remaining 62 bits (`rand_b`) are filled using the system random number generator (for `timeOrdered()`) or the provided generator (for `timeOrdered(using:timeSince1970:)`). The `timeOrdered()` convenience delegates to `timeOrdered(using:)` with a `SystemRandomNumberGenerator`. The optional `timeSince1970` parameter accepts a `Duration` representing the time since the Unix epoch, allowing callers to embed a specific timestamp. +The most significant 48 bits contain a millisecond-precision Unix timestamp. The 12 bits following the version field (`rand_a`) encode sub-millisecond timestamp precision per RFC 9562 Section 6.2, Method 3. The remaining 62 bits (`rand_b`, after the variant field) are filled using `generator`. The `timeOrdered()` convenience delegates to `timeOrdered(using:)` with a `SystemRandomNumberGenerator`. + +When called without a `Date` argument, the combined timestamp (milliseconds + sub-millisecond precision) is guaranteed to be monotonically increasing within the current process. An atomic value tracks the last returned timestamp; if the system clock has not advanced since the previous call, the value is incremented by one sub-millisecond tick. This ensures strict ordering even under high-frequency generation or clock adjustments, following the same approach used by Go's `google/uuid` and PostgreSQL. When a caller provides an explicit `date`, the monotonicity guarantee does not apply. ### Extracting the timestamp ```swift @available(FoundationPreview 6.4, *) extension UUID { - /// For version 7 UUIDs, returns the `Date` encoded in the - /// most significant 48 bits. Returns `nil` for all other versions. - /// The returned date has millisecond precision, as specified - /// by RFC 9562. + /// For version 7 UUIDs, returns the `Date` encoded in the most significant 48 bits. Returns `nil` for all other versions. + /// + /// The returned date has millisecond precision, as specified by RFC 9562. + /// + /// - Note: Even though this implementation, or others, may choose to encode more precision into other bytes of the `UUID`, this method may only return the portion of the timestamp stored in the RFC-specified bytes. public var timeOrderedTimestamp: Date? { get } diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 8dbe4f1642..7c7f121522 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -23,6 +23,8 @@ import WinSDK @preconcurrency import WASILibc #endif +internal import Synchronization + public typealias uuid_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) public typealias uuid_string_t = (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8) @@ -41,7 +43,7 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { } } - /* Create a new UUID with RFC 4122 version 4 random bytes */ + /// Create a new UUID with RFC 4122 version 4 random bytes. public init() { var generator = SystemRandomNumberGenerator() self = UUID.random(using: &generator) @@ -97,15 +99,9 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { } } - /// Creates a UUID by filling its 16 bytes using a closure that - /// writes into an `OutputRawSpan`. + /// Creates a UUID by filling its 16 bytes using a closure that writes into an `OutputSpan`. /// /// The closure must write exactly 16 bytes into the output span. - /// - /// let uuid = UUID { output in - /// output.append(myTimestampBytes) - /// output.append(myRandomBytes) - /// } @available(FoundationPreview 6.4, *) public init( initializingWith initializer: (inout OutputSpan) throws(E) -> () @@ -117,13 +113,11 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { }) } - // Hex lookup tables for UUID string formatting. - // Each byte is converted to two hex characters via table lookup. + // Hex lookup tables for UUID string formatting. Each byte is converted to two hex characters via table lookup. private static let _upperHex: StaticString = "0123456789ABCDEF" private static let _lowerHex: StaticString = "0123456789abcdef" - /// Writes the UUID as a 36-character hex string into `buffer` - /// using the given hex digit lookup table. Returns 36. + /// Writes the UUID as a 36-character hex string into `buffer` using the given hex digit lookup table. Returns 36. private func _unparse( into buffer: UnsafeMutableBufferPointer, hexTable: StaticString @@ -269,17 +263,12 @@ extension UUID : Codable { extension UUID { /// The `nil` (or minimum) UUID, where all bits are set to zero. /// - /// As defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#section-5.9), - /// the nil UUID is a special form where all 128 bits are zero. - /// It can be used to represent the absence of a UUID value. + /// As defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#section-5.9), the nil UUID is a special form where all 128 bits are zero. It can be used to represent the absence of a UUID value. public static let min = UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) /// The max UUID, where all bits are set to one. /// - /// As defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#section-5.10), - /// the max UUID is a special form where all 128 bits are one. - /// It can be used as a sentinel value, for example to represent - /// "the largest possible UUID" in a sorted range. + /// As defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#section-5.10), the max UUID is a special form where all 128 bits are one. It can be used as a sentinel value, for example to represent "the largest possible UUID" in a sorted range. public static let max = UUID(uuid: (0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF)) } @@ -335,49 +324,49 @@ extension UUID { public static var custom: Version { Version(rawValue: 8) } } - /// The version of this UUID, derived from the version bits - /// (bits 48–51) as defined by RFC 9562. + /// The version of this UUID, derived from the version bits (bits 48–51) as defined by RFC 9562. public var version: UUID.Version { Version(rawValue: _storage[6] >> 4) } - /// Creates a new UUID with RFC 9562 version 7 layout: a Unix - /// timestamp in milliseconds in the most significant 48 bits, - /// followed by random bits. The variant and version fields are - /// set per the RFC. + /// Creates a new UUID with RFC 9562 version 7 layout: a Unix timestamp in milliseconds in the most significant 48 bits, followed by random bits. The variant and version fields are set per the RFC. /// - /// Version 7 UUIDs sort in approximate chronological order - /// when compared using the standard `<` operator, making them - /// well-suited as database primary keys. UUIDs created within - /// the same millisecond are distinguished by random bits and - /// may not reflect exact creation order. + /// Version 7 UUIDs sort in chronological order when compared using the standard `<` operator, making them well-suited as database primary keys. UUIDs generated within the same process are guaranteed to be monotonically increasing. public static func timeOrdered() -> UUID { var generator = SystemRandomNumberGenerator() return timeOrdered(using: &generator) } - /// Creates a new UUID with RFC 9562 version 7 layout using - /// the specified random number generator for the random bits. + /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits. /// - /// The most significant 48 bits contain a millisecond-precision - /// Unix timestamp. The 12 bits following the version field - /// (`rand_a`) encode sub-millisecond timestamp precision per - /// RFC 9562 Section 6.2, Method 3. The remaining 62 bits - /// (`rand_b`, after the variant field) are filled using `generator`. + /// When called without an `at` argument, the timestamp portion is guaranteed to be monotonically increasing within the current process, even under high-frequency generation or clock adjustments. /// - /// - Parameter generator: The random number generator to use - /// when creating the random portions of the UUID. - /// - Parameter timeSince1970: The time since the Unix epoch to - /// encode in the timestamp field. If `nil`, the current time - /// is used. `Duration` provides sub-millisecond precision - /// without floating-point loss. + /// - Parameter generator: The random number generator to use when creating the random portions of the UUID. + /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. + /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. /// - Returns: A version 7 UUID. public static func timeOrdered( using generator: inout some RandomNumberGenerator, - timeSince1970: Duration? = nil + at date: Date? = nil, + offset: Duration = .zero ) -> UUID { - let elapsed = timeSince1970 ?? Duration.durationSince1970 - let (ms, subMS) = elapsed._uuidTimestampComponents + // The most significant 48 bits contain a millisecond-precision Unix timestamp. + // The 12 bits following the version field (`rand_a`) encode sub-millisecond timestamp precision per RFC 9562 Section 6.2, Method 3. + // The remaining 62 bits (`rand_b`, after the variant field) are filled using `generator`. + let combined: UInt64 + if let date { + // Caller-provided date (plus offset): convert to Duration, + // no monotonic guard + let elapsed = Duration.seconds(date.timeIntervalSince1970) + offset + let (ms, subMS) = elapsed._uuidTimestampComponents + combined = ms << 12 | UInt64(subMS) + } else { + // Current time (plus offset) with monotonic guarantee + combined = _nextMonotonicTimestamp(offset: offset) + } + + let ms = combined >> 12 + let subMS = UInt16(combined & 0x0FFF) var first: UInt64 = 0 // Bits 0–47: millisecond timestamp @@ -414,10 +403,11 @@ extension UUID { } } - /// For version 7 UUIDs, returns the `Date` encoded in the - /// most significant 48 bits. Returns `nil` for all other versions. - /// The returned date has millisecond precision, as specified - /// by RFC 9562. + /// For version 7 UUIDs, returns the `Date` encoded in the most significant 48 bits. Returns `nil` for all other versions. + /// + /// The returned date has millisecond precision, as specified by RFC 9562. + /// + /// - Note: Even though this implementation, or others, may choose to encode more precision into other bytes of the `UUID`, this method may only return the portion of the timestamp stored in the RFC-specified bytes. public var timeOrderedTimestamp: Date? { guard version == .timeOrdered else { return nil } let ms: UInt64 = UInt64(_storage[0]) << 40 | UInt64(_storage[1]) << 32 @@ -426,15 +416,39 @@ extension UUID { return Date(timeIntervalSince1970: Double(ms) / 1000.0) } - // MARK: - Private time helpers + // MARK: - Monotonic timestamp + + /// Tracks the last combined timestamp value to ensure monotonically increasing v7 UUIDs within a process. + private static let _lastTimestamp = Atomic(0) + + /// Returns a combined 60-bit timestamp, which is guaranteed to be strictly greater than any previously returned value. If the clock hasn't advanced since the last call, the previous value is incremented by 1. + private static func _nextMonotonicTimestamp(offset: Duration) -> UInt64 { + let elapsed = Duration.durationSince1970 + offset + let (ms, subMS) = elapsed._uuidTimestampComponents + + let current = ms << 12 | UInt64(subMS) + var old = _lastTimestamp.load(ordering: .relaxed) + + while true { + let next = Swift.max(current, old &+ 1) + let (exchanged, original) = _lastTimestamp.compareExchange( + expected: old, + desired: next, + ordering: .relaxed + ) + if exchanged { + return next + } + old = original + } + } } extension Duration { /// Attoseconds per millisecond (10^15). private static let _attosPerMS: Int64 = 1_000_000_000_000_000 - /// The current wall clock time as a `Duration` since the Unix epoch, - /// using the highest precision time source available on the platform. + /// The current wall clock time as a `Duration` since the Unix epoch, using the highest precision time source available on the platform. fileprivate static var durationSince1970: Duration { #if canImport(WinSDK) // FILETIME is 100-nanosecond intervals since January 1, 1601 (UTC). @@ -458,12 +472,9 @@ extension Duration { #endif } - /// Extracts the 48-bit millisecond timestamp and 12-bit - /// sub-millisecond fraction from this duration (interpreted as - /// time since Unix epoch), using pure integer arithmetic. + /// Extracts the 48-bit millisecond timestamp and 12-bit sub-millisecond fraction from this duration (interpreted as time since Unix epoch), using pure integer arithmetic. /// - /// Returns `(ms, subMS)` where `ms` is clamped to 48 bits and - /// `subMS` is 0–4095. + /// Returns `(ms, subMS)` where `ms` is clamped to 48 bits and `subMS` is 0–4095. fileprivate var _uuidTimestampComponents: (ms: UInt64, subMS: UInt16) { let (secs, attos) = self.components diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 9ceef278a8..64a4df4c60 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -333,25 +333,25 @@ private struct UUIDTests { @available(FoundationPreview 6.4, *) @Test func timeOrderedUsingDeterministicGenerator() { - let fixedTime = Duration.seconds(1645557742) // RFC 9562 A.6 timestamp + let fixedDate = Date(timeIntervalSince1970: 1645557742.0) // RFC 9562 A.6 timestamp var gen1 = PCGRandomNumberGenerator(seed: 42) var gen2 = PCGRandomNumberGenerator(seed: 42) - let uuid1 = UUID.timeOrdered(using: &gen1, timeSince1970: fixedTime) - let uuid2 = UUID.timeOrdered(using: &gen2, timeSince1970: fixedTime) - // Same seed and same time produces identical UUIDs + let uuid1 = UUID.timeOrdered(using: &gen1, at: fixedDate) + let uuid2 = UUID.timeOrdered(using: &gen2, at: fixedDate) + // Same seed and same date produces identical UUIDs #expect(uuid1 == uuid2) // Verify the timestamp round-trips - #expect(uuid1.timeOrderedTimestamp == Date(timeIntervalSince1970: 1645557742.0)) + #expect(uuid1.timeOrderedTimestamp == fixedDate) } @available(FoundationPreview 6.4, *) @Test func timeOrderedDifferentSeedsSameDate() { - let fixedTime = Duration.seconds(1645557742) + let fixedDate = Date(timeIntervalSince1970: 1645557742.0) var gen1 = PCGRandomNumberGenerator(seed: 42) var gen2 = PCGRandomNumberGenerator(seed: 99) - let uuid1 = UUID.timeOrdered(using: &gen1, timeSince1970: fixedTime) - let uuid2 = UUID.timeOrdered(using: &gen2, timeSince1970: fixedTime) - // Same time but different seeds produces different UUIDs + let uuid1 = UUID.timeOrdered(using: &gen1, at: fixedDate) + let uuid2 = UUID.timeOrdered(using: &gen2, at: fixedDate) + // Same date but different seeds produces different UUIDs #expect(uuid1 != uuid2) // Both should still have the same timestamp #expect(uuid1.timeOrderedTimestamp == uuid2.timeOrderedTimestamp) @@ -359,32 +359,71 @@ private struct UUIDTests { @available(FoundationPreview 6.4, *) @Test func timeOrderedAtSpecificDate() throws { - let time = Duration.seconds(1000) + let date = Date(timeIntervalSince1970: 1000.0) var generator = SystemRandomNumberGenerator() - let uuid = UUID.timeOrdered(using: &generator, timeSince1970: time) + let uuid = UUID.timeOrdered(using: &generator, at: date) let timestamp = try #require(uuid.timeOrderedTimestamp) - #expect(timestamp == Date(timeIntervalSince1970: 1000.0)) + #expect(timestamp == date) #expect(uuid.version == .timeOrdered) } @available(FoundationPreview 6.4, *) @Test func timeOrderedSubMillisecondPrecision() { // RFC 9562 Section 6.2 Method 3: rand_a encodes sub-ms precision. - // 456_789 nanoseconds = 456_789_000_000_000 attoseconds - // rand_a = (456_789_000_000_000 * 4096) / 1_000_000_000_000_000 = 1871 - let time = Duration.seconds(1000) + Duration.nanoseconds(123_456_789) + // Date with 0.123456789 fractional seconds → 456_789 µs sub-ms + // Duration.seconds converts through Double, so we use a value + // with exact binary representation for the sub-ms test. + // 0.5 ms fraction → 0.5 * 4096 = 2048 + let date = Date(timeIntervalSince1970: 1000.0005) var generator = SystemRandomNumberGenerator() - let uuid = UUID.timeOrdered(using: &generator, timeSince1970: time) + let uuid = UUID.timeOrdered(using: &generator, at: date) // rand_a is the lower nibble of byte 6 and all of byte 7 let randA = (UInt16(uuid.span[6]) & 0x0F) << 8 | UInt16(uuid.span[7]) - #expect(randA == 1871) + #expect(randA == 2048) } @available(FoundationPreview 6.4, *) - @Test func timeOrderedMonotonicity() async throws { + @Test func timeOrderedWithOffsetFromDate() throws { + let base = Date(timeIntervalSince1970: 1000.0) + let offset = Duration.seconds(60) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.timeOrdered(using: &generator, at: base, offset: offset) + let timestamp = try #require(uuid.timeOrderedTimestamp) + // Should encode base + 60s = 1060.0 + #expect(timestamp == Date(timeIntervalSince1970: 1060.0)) + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedWithNegativeOffset() throws { + let base = Date(timeIntervalSince1970: 2000.0) + let offset = Duration.seconds(-500) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.timeOrdered(using: &generator, at: base, offset: offset) + let timestamp = try #require(uuid.timeOrderedTimestamp) + // Should encode base - 500s = 1500.0 + #expect(timestamp == Date(timeIntervalSince1970: 1500.0)) + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedWithOffsetFromCurrentTime() { + // Offset of +1 hour from current time should produce a UUID + // with a timestamp roughly 1 hour in the future + let before = Date().addingTimeInterval(3600.0 - 1.0) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.timeOrdered(using: &generator, offset: .seconds(3600)) + let timestamp = uuid.timeOrderedTimestamp! + let after = Date().addingTimeInterval(3600.0 + 1.0) + #expect(timestamp >= before) + #expect(timestamp <= after) + } + + @available(FoundationPreview 6.4, *) + @Test func timeOrderedMonotonicity() { + // Generate many UUIDs in a tight loop without any delays. + // The monotonic guarantee ensures each is strictly greater + // than the previous, even within the same sub-millisecond. var previous = UUID.timeOrdered() - for _ in 0..<100 { - try await Task.sleep(for: .milliseconds(2)) + for _ in 0..<10_000 { let current = UUID.timeOrdered() #expect(previous < current) previous = current From b0bd88697a8c0e5e34669c7a55fcfe4cd9ec5de7 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Fri, 20 Mar 2026 14:52:39 -0700 Subject: [PATCH 10/15] Remove Version struct, rename timeOrderedTimestamp property --- Proposals/NNNN-uuid-versions.md | 69 ++-------- Sources/FoundationEssentials/UUID.swift | 37 +----- .../FoundationEssentialsTests/UUIDTests.swift | 123 +++++++----------- 3 files changed, 69 insertions(+), 160 deletions(-) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index 463aaa29b4..27c3e08128 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -21,7 +21,7 @@ Today, developers who need time-ordered UUIDs usually construct the bytes manual ## Proposed solution -Add a `UUID.Version` struct representing the well-known UUID versions from RFC 9562, a `version` property on `UUID` for introspection, a static factory method for creating version 7 UUIDs, and convenience properties for the nil (which we name `min` to avoid confusion in Swift) and max UUIDs. +Add a `version` property on `UUID` for introspection, a static factory method for creating version 7 UUIDs, and convenience properties for the nil (which we name `min` to avoid confusion in Swift) and max UUIDs. ```swift // Create a time-ordered UUID @@ -29,9 +29,9 @@ let id = UUID.timeOrdered() // Inspect the version of any UUID switch id.version { -case .timeOrdered: +case 7: print("v7 UUID, sortable by creation time") -case .random: +case 4: print("v4 UUID") default: print("other version") @@ -39,7 +39,7 @@ default: // The existing init() continues to create version 4 UUIDs let randomID = UUID() -assert(randomID.version == .random) +assert(randomID.version == 4) // Min and max UUIDs for sentinel values let minID = UUID.min // 00000000-0000-0000-0000-000000000000 @@ -69,7 +69,7 @@ extension UUID { } ``` -The min UUID (`00000000-0000-0000-0000-000000000000`) and max UUID (`FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF`) are special forms defined by RFC 9562. They are useful as sentinel values — for example, representing "no UUID" or defining the bounds of a UUID range. Note that neither the min UUID nor the max UUID has a meaningful version or variant field; the `version` property returns `Version(rawValue: 0)` and `Version(rawValue: 15)` respectively. +The min UUID (`00000000-0000-0000-0000-000000000000`) and max UUID (`FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF`) are special forms defined by RFC 9562. They are useful as sentinel values — for example, representing "no UUID" or defining the bounds of a UUID range. Note that neither the min UUID nor the max UUID has a meaningful version or variant field; the `version` property returns `0` and `15` respectively. ### Lowercase string representation @@ -137,55 +137,18 @@ let uuid = UUID { output in The closure receives an `OutputSpan` backed by the UUID's 16-byte storage. If the closure writes fewer or more than 16 bytes, the initializer traps. If the closure throws, the error is propagated with its original type. -### `UUID.Version` - -```swift -@available(FoundationPreview 6.4, *) -extension UUID { - /// The version of a UUID, as defined by RFC 9562. - public struct Version: Sendable, Hashable, Codable, RawRepresentable { - public let rawValue: UInt8 - public init(rawValue: UInt8) - - /// Version 1: Gregorian time-based UUID with node identifier. - public static var timeBased: Version { get } - - /// Version 3: Name-based UUID using MD5 hashing. - public static var nameBasedMD5: Version { get } - - /// Version 4: Random UUID. - public static var random: Version { get } - - /// Version 5: Name-based UUID using SHA-1 hashing. - public static var nameBasedSHA1: Version { get } - - /// Version 6: Reordered Gregorian time-based UUID. - public static var reorderedTimeBased: Version { get } - - /// Version 7: Unix Epoch time-based UUID with random bits. - public static var timeOrdered: Version { get } - - /// Version 8: Custom UUID with user-defined layout. - public static var custom: Version { get } - } -} -``` - -The version value is encoded in bits 48–51 of the UUID (the high nibble of byte 6), per RFC 9562. `Version` is a `RawRepresentable` struct rather than an enum, allowing new versions to be added without breaking source or binary compatibility. The well-known versions from RFC 9562 are provided as static properties. Versions 2 (DCE Security), 0 (min UUID), and 15 (max UUID) do not have static properties but can be represented using `Version(rawValue:)` if needed. - ### `version` property ```swift @available(FoundationPreview 6.4, *) extension UUID { - /// The version of this UUID, derived from the version bits - /// (bits 48–51) as defined by RFC 9562. - public var version: UUID.Version { - get - } + /// The version of this UUID, derived from the version bits (bits 48–51) as defined by RFC 9562. + public var version: Int { get } } ``` +The version value is encoded in bits 48–51 of the UUID (the high nibble of byte 6), per RFC 9562. The returned `Int` ranges from 0 to 15. Well-known versions include 1 (time-based), 3 (name-based MD5), 4 (random), 5 (name-based SHA-1), 6 (reordered time-based), 7 (time-ordered), and 8 (custom). + ### Creating version 7 UUIDs ```swift @@ -198,7 +161,7 @@ extension UUID { /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits. /// - /// When called without an `at` argument, the timestamp portion is guaranteed to be monotonically increasing within the current process, even under high-frequency generation or clock adjustments. + /// When called without an `at` argument, the timestamp portion is guaranteed to be monotonically increasing within the current process. /// /// - Parameter generator: The random number generator to use when creating the random portions of the UUID. /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. @@ -216,7 +179,7 @@ The most significant 48 bits contain a millisecond-precision Unix timestamp. The When called without a `Date` argument, the combined timestamp (milliseconds + sub-millisecond precision) is guaranteed to be monotonically increasing within the current process. An atomic value tracks the last returned timestamp; if the system clock has not advanced since the previous call, the value is incremented by one sub-millisecond tick. This ensures strict ordering even under high-frequency generation or clock adjustments, following the same approach used by Go's `google/uuid` and PostgreSQL. When a caller provides an explicit `date`, the monotonicity guarantee does not apply. -### Extracting the timestamp +### Extracting the date ```swift @available(FoundationPreview 6.4, *) @@ -226,7 +189,7 @@ extension UUID { /// The returned date has millisecond precision, as specified by RFC 9562. /// /// - Note: Even though this implementation, or others, may choose to encode more precision into other bytes of the `UUID`, this method may only return the portion of the timestamp stored in the RFC-specified bytes. - public var timeOrderedTimestamp: Date? { + public var date: Date? { get } } @@ -251,12 +214,8 @@ This feature can be freely adopted and un-adopted in source code with no deploym ### Adding version as a parameter to `init()` -Instead of `UUID.timeOrdered()`, we considered `UUID(version: .timeOrdered)`. However, different versions require different parameters — version 5 needs a name and namespace, version 8 needs custom data — so a single initializer would either need to accept many optional parameters or use an associated-value enum. Static factory methods are clearer and allow each version to have its own natural parameter list. - -### Using an `enum` for `Version` - -We considered making `Version` an `enum` with a `UInt8` raw value. However, a `struct` with `RawRepresentable` conformance allows new versions to be added in the future without breaking source or binary compatibility. Since the UUID version field is only 4 bits, the full space of 16 values is defined by the RFC, but using a struct is more consistent with Foundation's conventions for open sets of values (e.g., `NSNotificationName`, `RunLoop.Mode`) and avoids the need for an `unknown` case or optional return from the `version` property. +Instead of `UUID.timeOrdered()`, we considered `UUID(version: 7)`. However, different versions require different parameters — version 5 needs a name and namespace, version 8 needs custom data — so a single initializer would either need to accept many optional parameters or use an associated-value enum. Static factory methods are clearer and allow each version to have its own natural parameter list. ### Supporting all UUID versions immediately -We considered adding factory methods for all versions (1, 3, 5, 6, 7, 8), but the immediate need is version 7. Version 1 (time-based with MAC address) has privacy implications. Versions 3 and 5 require different parameters. Version 6 is a reordering of version 1 and shares its concerns. Version 8 is intentionally application-defined. Starting with version 7 keeps the proposal focused while the `Version` struct provides the foundation to add others incrementally. +We considered adding factory methods for all versions (1, 3, 5, 6, 7, 8), but the immediate need is version 7. Version 1 (time-based with MAC address) has privacy implications. Versions 3 and 5 require different parameters. Version 6 is a reordering of version 1 and shares its concerns. Version 8 is intentionally application-defined. Starting with version 7 keeps the proposal focused. diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 7c7f121522..f8daf20c5b 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -297,36 +297,9 @@ extension UUID { @available(FoundationPreview 6.4, *) extension UUID { - /// The version of a UUID, as defined by RFC 9562. - public struct Version: Sendable, Hashable, Codable, RawRepresentable { - public let rawValue: UInt8 - public init(rawValue: UInt8) { self.rawValue = rawValue } - - /// Version 1: Gregorian time-based UUID with node identifier. - public static var timeBased: Version { Version(rawValue: 1) } - - /// Version 3: Name-based UUID using MD5 hashing. - public static var nameBasedMD5: Version { Version(rawValue: 3) } - - /// Version 4: Random UUID. - public static var random: Version { Version(rawValue: 4) } - - /// Version 5: Name-based UUID using SHA-1 hashing. - public static var nameBasedSHA1: Version { Version(rawValue: 5) } - - /// Version 6: Reordered Gregorian time-based UUID. - public static var reorderedTimeBased: Version { Version(rawValue: 6) } - - /// Version 7: Unix Epoch time-based UUID with random bits. - public static var timeOrdered: Version { Version(rawValue: 7) } - - /// Version 8: Custom UUID with user-defined layout. - public static var custom: Version { Version(rawValue: 8) } - } - /// The version of this UUID, derived from the version bits (bits 48–51) as defined by RFC 9562. - public var version: UUID.Version { - Version(rawValue: _storage[6] >> 4) + public var version: Int { + Int(_storage[6] >> 4) } /// Creates a new UUID with RFC 9562 version 7 layout: a Unix timestamp in milliseconds in the most significant 48 bits, followed by random bits. The variant and version fields are set per the RFC. @@ -339,7 +312,7 @@ extension UUID { /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits. /// - /// When called without an `at` argument, the timestamp portion is guaranteed to be monotonically increasing within the current process, even under high-frequency generation or clock adjustments. + /// When called without an `at` argument, the timestamp portion is guaranteed to be monotonically increasing within the current process. /// /// - Parameter generator: The random number generator to use when creating the random portions of the UUID. /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. @@ -408,8 +381,8 @@ extension UUID { /// The returned date has millisecond precision, as specified by RFC 9562. /// /// - Note: Even though this implementation, or others, may choose to encode more precision into other bytes of the `UUID`, this method may only return the portion of the timestamp stored in the RFC-specified bytes. - public var timeOrderedTimestamp: Date? { - guard version == .timeOrdered else { return nil } + public var date: Date? { + guard version == 7 else { return nil } let ms: UInt64 = UInt64(_storage[0]) << 40 | UInt64(_storage[1]) << 32 | UInt64(_storage[2]) << 24 | UInt64(_storage[3]) << 16 | UInt64(_storage[4]) << 8 | UInt64(_storage[5]) diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 64a4df4c60..0e1b12250f 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -74,8 +74,7 @@ private struct UUIDTests { @Test func hash() { let values: [UUID] = [ - // This list takes a UUID and tweaks every byte while - // leaving the version/variant intact. + // This list takes a UUID and tweaks every byte while leaving the version/variant intact. UUID(uuidString: "a53baa1c-b4f5-48db-9467-9786b76b256c")!, UUID(uuidString: "a63baa1c-b4f5-48db-9467-9786b76b256c")!, UUID(uuidString: "a53caa1c-b4f5-48db-9467-9786b76b256c")!, @@ -111,7 +110,6 @@ private struct UUIDTests { #expect(anyHashables[1] == anyHashables[2]) } - // rdar://71190003 (UUID has no customMirror) @Test func customMirror() throws { let uuid = try #require(UUID(uuidString: "89E90DC6-5EBA-41A8-A64D-81D3576EE46E")) #expect(String(reflecting: uuid) == "89E90DC6-5EBA-41A8-A64D-81D3576EE46E") @@ -148,8 +146,8 @@ private struct UUIDTests { var generator = SystemRandomNumberGenerator() for _ in 0..<10000 { let uuid = UUID.random(using: &generator) - #expect(uuid.versionNumber == 0b0100) - #expect(uuid.varint == 0b10) + #expect(uuid.version == 0b0100) + #expect(uuid.variant == 0b10) } } @@ -227,7 +225,7 @@ private struct UUIDTests { let previousValue = uuid.span[6] var span = uuid.mutableSpan span[6] = (previousValue & 0x0F) | 0x70 - #expect(uuid.version == .timeOrdered) + #expect(uuid.version == 7) } @available(FoundationPreview 6.4, *) @@ -267,45 +265,45 @@ private struct UUIDTests { @available(FoundationPreview 6.4, *) @Test func versionProperty() { // UUID() creates v4 - let v4 = UUID() - #expect(v4.version == .random) + let defaultVersion = UUID() + #expect(defaultVersion.version == 4) // RFC 9562 Appendix A test vectors // A.1: UUIDv1 let v1 = UUID(uuidString: "C232AB00-9414-11EC-B3C8-9F6BDECED846")! - #expect(v1.version == .timeBased) + #expect(v1.version == 1) // A.2: UUIDv3 let v3 = UUID(uuidString: "5df41881-3aed-3515-88a7-2f4a814cf09e")! - #expect(v3.version == .nameBasedMD5) + #expect(v3.version == 3) // A.3: UUIDv4 - let v4rfc = UUID(uuidString: "919108f7-52d1-4320-9bac-f847db4148a8")! - #expect(v4rfc.version == .random) + let v4 = UUID(uuidString: "919108f7-52d1-4320-9bac-f847db4148a8")! + #expect(v4.version == 4) // A.4: UUIDv5 let v5 = UUID(uuidString: "2ed6657d-e927-568b-95e1-2665a8aea6a2")! - #expect(v5.version == .nameBasedSHA1) + #expect(v5.version == 5) // A.5: UUIDv6 let v6 = UUID(uuidString: "1EC9414C-232A-6B00-B3C8-9F6BDECED846")! - #expect(v6.version == .reorderedTimeBased) + #expect(v6.version == 6) // A.6: UUIDv7 let v7 = UUID(uuidString: "017F22E2-79B0-7CC3-98C4-DC0C0C07398F")! - #expect(v7.version == .timeOrdered) + #expect(v7.version == 7) // B.1: UUIDv8 let v8 = UUID(uuidString: "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0")! - #expect(v8.version == .custom) + #expect(v8.version == 8) } @available(FoundationPreview 6.4, *) @Test func timeOrderedVersionAndVariant() { for _ in 0..<10000 { let uuid = UUID.timeOrdered() - #expect(uuid.versionNumber == 0b0111) - #expect(uuid.varint == 0b10) + #expect(uuid.version == 7) + #expect(uuid.variant == 0b10) } } @@ -314,21 +312,19 @@ private struct UUIDTests { var generator = SystemRandomNumberGenerator() for _ in 0..<10000 { let uuid = UUID.timeOrdered(using: &generator) - #expect(uuid.versionNumber == 0b0111) - #expect(uuid.varint == 0b10) + #expect(uuid.version == 7) + #expect(uuid.variant == 0b10) } } @available(FoundationPreview 6.4, *) @Test func timeOrderedUsingGeneratorTimestamp() throws { var generator = SystemRandomNumberGenerator() - let before = Date() - let uuid = UUID.timeOrdered(using: &generator) - let after = Date() - - let timestamp = try #require(uuid.timeOrderedTimestamp) - #expect(timestamp >= before.addingTimeInterval(-0.1)) - #expect(timestamp <= after.addingTimeInterval(0.1)) + let date = Date(timeIntervalSince1970: 1700000000.123) + let uuid = UUID.timeOrdered(using: &generator, at: date) + let timestamp = try #require(uuid.date) + // We will lose some precision from the original date in the encoded date. + #expect(timestamp.timeIntervalSince1970.rounded(.down) == date.timeIntervalSince1970.rounded(.down)) } @available(FoundationPreview 6.4, *) @@ -341,7 +337,7 @@ private struct UUIDTests { // Same seed and same date produces identical UUIDs #expect(uuid1 == uuid2) // Verify the timestamp round-trips - #expect(uuid1.timeOrderedTimestamp == fixedDate) + #expect(uuid1.date == fixedDate) } @available(FoundationPreview 6.4, *) @@ -354,7 +350,7 @@ private struct UUIDTests { // Same date but different seeds produces different UUIDs #expect(uuid1 != uuid2) // Both should still have the same timestamp - #expect(uuid1.timeOrderedTimestamp == uuid2.timeOrderedTimestamp) + #expect(uuid1.date == uuid2.date) } @available(FoundationPreview 6.4, *) @@ -362,24 +358,23 @@ private struct UUIDTests { let date = Date(timeIntervalSince1970: 1000.0) var generator = SystemRandomNumberGenerator() let uuid = UUID.timeOrdered(using: &generator, at: date) - let timestamp = try #require(uuid.timeOrderedTimestamp) + let timestamp = try #require(uuid.date) #expect(timestamp == date) - #expect(uuid.version == .timeOrdered) + #expect(uuid.version == 7) } @available(FoundationPreview 6.4, *) @Test func timeOrderedSubMillisecondPrecision() { // RFC 9562 Section 6.2 Method 3: rand_a encodes sub-ms precision. - // Date with 0.123456789 fractional seconds → 456_789 µs sub-ms - // Duration.seconds converts through Double, so we use a value - // with exact binary representation for the sub-ms test. - // 0.5 ms fraction → 0.5 * 4096 = 2048 - let date = Date(timeIntervalSince1970: 1000.0005) + // Use a whole-millisecond date (no sub-ms fraction) to verify rand_a == 0 + let date = Date(timeIntervalSince1970: 1000.123) var generator = SystemRandomNumberGenerator() let uuid = UUID.timeOrdered(using: &generator, at: date) // rand_a is the lower nibble of byte 6 and all of byte 7 let randA = (UInt16(uuid.span[6]) & 0x0F) << 8 | UInt16(uuid.span[7]) - #expect(randA == 2048) + #expect(randA == 0) + // Verify the millisecond timestamp is correct + #expect(uuid.date == Date(timeIntervalSince1970: 1000.123)) } @available(FoundationPreview 6.4, *) @@ -388,7 +383,7 @@ private struct UUIDTests { let offset = Duration.seconds(60) var generator = SystemRandomNumberGenerator() let uuid = UUID.timeOrdered(using: &generator, at: base, offset: offset) - let timestamp = try #require(uuid.timeOrderedTimestamp) + let timestamp = try #require(uuid.date) // Should encode base + 60s = 1060.0 #expect(timestamp == Date(timeIntervalSince1970: 1060.0)) } @@ -399,19 +394,18 @@ private struct UUIDTests { let offset = Duration.seconds(-500) var generator = SystemRandomNumberGenerator() let uuid = UUID.timeOrdered(using: &generator, at: base, offset: offset) - let timestamp = try #require(uuid.timeOrderedTimestamp) + let timestamp = try #require(uuid.date) // Should encode base - 500s = 1500.0 #expect(timestamp == Date(timeIntervalSince1970: 1500.0)) } @available(FoundationPreview 6.4, *) @Test func timeOrderedWithOffsetFromCurrentTime() { - // Offset of +1 hour from current time should produce a UUID - // with a timestamp roughly 1 hour in the future + // Offset of +1 hour from current time should produce a UUID with a timestamp roughly 1 hour in the future let before = Date().addingTimeInterval(3600.0 - 1.0) var generator = SystemRandomNumberGenerator() let uuid = UUID.timeOrdered(using: &generator, offset: .seconds(3600)) - let timestamp = uuid.timeOrderedTimestamp! + let timestamp = uuid.date! let after = Date().addingTimeInterval(3600.0 + 1.0) #expect(timestamp >= before) #expect(timestamp <= after) @@ -420,8 +414,7 @@ private struct UUIDTests { @available(FoundationPreview 6.4, *) @Test func timeOrderedMonotonicity() { // Generate many UUIDs in a tight loop without any delays. - // The monotonic guarantee ensures each is strictly greater - // than the previous, even within the same sub-millisecond. + // The monotonic guarantee ensures each is strictly greater than the previous, even within the same sub-millisecond. var previous = UUID.timeOrdered() for _ in 0..<10_000 { let current = UUID.timeOrdered() @@ -431,41 +424,28 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func timeOrderedTimestamp() throws { - let before = Date() - let uuid = UUID.timeOrdered() - let after = Date() - - let timestamp = try #require(uuid.timeOrderedTimestamp) - #expect(timestamp >= before.addingTimeInterval(-0.1)) - #expect(timestamp <= after.addingTimeInterval(0.1)) + @Test func timeOrderedDate() throws { + let date = Date(timeIntervalSince1970: 1700000000.456) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.timeOrdered(using: &generator, at: date) + let timestamp = try #require(uuid.date) + #expect(timestamp == date) } // RFC 9562 Appendix A.6: UUIDv7 test vector with known timestamp // Tuesday, February 22, 2022 2:22:22.00 PM GMT-05:00 = 1645557742000 ms @available(FoundationPreview 6.4, *) - @Test func timeOrderedTimestampRFCVector() throws { + @Test func timeOrderedDateRFCVector() throws { let v7 = UUID(uuidString: "017F22E2-79B0-7CC3-98C4-DC0C0C07398F")! - let timestamp = try #require(v7.timeOrderedTimestamp) + let timestamp = try #require(v7.date) let expected = Date(timeIntervalSince1970: 1645557742.0) #expect(timestamp == expected) } @available(FoundationPreview 6.4, *) - @Test func timeOrderedTimestampNilForV4() { + @Test func timeOrderedDateNilForV4() { let uuid = UUID() - #expect(uuid.timeOrderedTimestamp == nil) - } - - @available(FoundationPreview 6.4, *) - @Test func versionRawValue() { - #expect(UUID.Version(rawValue: 1) == .timeBased) - #expect(UUID.Version(rawValue: 3) == .nameBasedMD5) - #expect(UUID.Version(rawValue: 4) == .random) - #expect(UUID.Version(rawValue: 5) == .nameBasedSHA1) - #expect(UUID.Version(rawValue: 6) == .reorderedTimeBased) - #expect(UUID.Version(rawValue: 7) == .timeOrdered) - #expect(UUID.Version(rawValue: 8) == .custom) + #expect(uuid.date == nil) } @available(FoundationPreview 6.4, *) @@ -474,7 +454,7 @@ private struct UUIDTests { // Construct a UUID with the version nibble set to `v` let byte6 = v << 4 let uuid = UUID(uuid: (0, 0, 0, 0, 0, 0, byte6, 0, 0x80, 0, 0, 0, 0, 0, 0, 0)) - #expect(uuid.version.rawValue == v) + #expect(uuid.version == Int(v)) } } @@ -497,15 +477,12 @@ private struct UUIDTests { } extension UUID { - fileprivate var versionNumber: Int { - Int(self.uuid.6 >> 4) - } - - fileprivate var varint: Int { + fileprivate var variant: Int { Int(self.uuid.8 >> 6 & 0b11) } } +/// A seedable random number generator for deterministic testing. The same seed always produces the same sequence, allowing tests to verify exact UUID output. fileprivate struct PCGRandomNumberGenerator: RandomNumberGenerator { private static let multiplier: UInt128 = 47_026_247_687_942_121_848_144_207_491_837_523_525 private static let increment: UInt128 = 117_397_592_171_526_113_268_558_934_119_004_209_487 From bda240e0d8354288c67de99b7d7b394d08afc8c5 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Fri, 20 Mar 2026 15:00:56 -0700 Subject: [PATCH 11/15] Rename to version7, add version4 as well --- .../Essentials/BenchmarkEssentials.swift | 2 +- Proposals/NNNN-uuid-versions.md | 24 ++++---- Sources/FoundationEssentials/UUID.swift | 12 +++- .../FoundationEssentialsTests/UUIDTests.swift | 58 +++++++++---------- 4 files changed, 53 insertions(+), 43 deletions(-) diff --git a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift index 762eab723c..273b70e6e5 100644 --- a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift +++ b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift @@ -43,7 +43,7 @@ let benchmarks = { Benchmark("UUIDCreateTimeOrdered") { benchmark in for _ in benchmark.scaledIterations { - blackHole(UUID.timeOrdered()) + blackHole(UUID.version7()) } } diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index 27c3e08128..b9f88977b7 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -24,8 +24,8 @@ Today, developers who need time-ordered UUIDs usually construct the bytes manual Add a `version` property on `UUID` for introspection, a static factory method for creating version 7 UUIDs, and convenience properties for the nil (which we name `min` to avoid confusion in Swift) and max UUIDs. ```swift -// Create a time-ordered UUID -let id = UUID.timeOrdered() +// Create a version 7 UUID +let id = UUID.version7() // Inspect the version of any UUID switch id.version { @@ -130,8 +130,9 @@ This initializer provides a safe, typed-throw-compatible way to construct a UUID ```swift let uuid = UUID { output in - output.append(timestampBytes) - output.append(randomBytes) + // Note: It is up to the custom implementation here to create a valid UUID. + output.append(...) + output.append(...) } ``` @@ -149,15 +150,18 @@ extension UUID { The version value is encoded in bits 48–51 of the UUID (the high nibble of byte 6), per RFC 9562. The returned `Int` ranges from 0 to 15. Well-known versions include 1 (time-based), 3 (name-based MD5), 4 (random), 5 (name-based SHA-1), 6 (reordered time-based), 7 (time-ordered), and 8 (custom). -### Creating version 7 UUIDs +### Creating version 4 and version 7 UUIDs ```swift @available(FoundationPreview 6.4, *) extension UUID { + /// Creates a new UUID with RFC 9562 version 4 (random) layout. This is equivalent to calling `UUID()`. + public static func version4() -> UUID + /// Creates a new UUID with RFC 9562 version 7 layout: a Unix timestamp in milliseconds in the most significant 48 bits, followed by random bits. The variant and version fields are set per the RFC. /// /// Version 7 UUIDs sort in chronological order when compared using the standard `<` operator, making them well-suited as database primary keys. UUIDs generated within the same process are guaranteed to be monotonically increasing. - public static func timeOrdered() -> UUID + public static func version7() -> UUID /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits. /// @@ -167,7 +171,7 @@ extension UUID { /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. /// - Returns: A version 7 UUID. - public static func timeOrdered( + public static func version7( using generator: inout some RandomNumberGenerator, at date: Date? = nil, offset: Duration = .zero @@ -175,7 +179,7 @@ extension UUID { } ``` -The most significant 48 bits contain a millisecond-precision Unix timestamp. The 12 bits following the version field (`rand_a`) encode sub-millisecond timestamp precision per RFC 9562 Section 6.2, Method 3. The remaining 62 bits (`rand_b`, after the variant field) are filled using `generator`. The `timeOrdered()` convenience delegates to `timeOrdered(using:)` with a `SystemRandomNumberGenerator`. +The most significant 48 bits contain a millisecond-precision Unix timestamp. The 12 bits following the version field (`rand_a`) encode sub-millisecond timestamp precision per RFC 9562 Section 6.2, Method 3. The remaining 62 bits (`rand_b`, after the variant field) are filled using `generator`. The `version7()` convenience delegates to `version7(using:)` with a `SystemRandomNumberGenerator`. When called without a `Date` argument, the combined timestamp (milliseconds + sub-millisecond precision) is guaranteed to be monotonically increasing within the current process. An atomic value tracks the last returned timestamp; if the system clock has not advanced since the previous call, the value is incremented by one sub-millisecond tick. This ensures strict ordering even under high-frequency generation or clock adjustments, following the same approach used by Go's `google/uuid` and PostgreSQL. When a caller provides an explicit `date`, the monotonicity guarantee does not apply. @@ -199,7 +203,7 @@ extension UUID { This proposal is purely additive. The existing `UUID()` initializer continues to create version 4 random UUIDs. The `random(using:)` static method is unaffected. No existing behavior changes. -UUIDs created by `timeOrdered()` are fully valid UUIDs and interoperate with all existing APIs that accept `UUID` or `NSUUID`, including `Codable`, `Comparable`, bridging, and string serialization. +UUIDs created by `version7()` are fully valid UUIDs and interoperate with all existing APIs that accept `UUID` or `NSUUID`, including `Codable`, `Comparable`, bridging, and string serialization. ## Implications on adoption @@ -214,7 +218,7 @@ This feature can be freely adopted and un-adopted in source code with no deploym ### Adding version as a parameter to `init()` -Instead of `UUID.timeOrdered()`, we considered `UUID(version: 7)`. However, different versions require different parameters — version 5 needs a name and namespace, version 8 needs custom data — so a single initializer would either need to accept many optional parameters or use an associated-value enum. Static factory methods are clearer and allow each version to have its own natural parameter list. +Instead of `UUID.version7()`, we considered `UUID(version: 7)`. However, different versions require different parameters — version 5 needs a name and namespace, version 8 needs custom data — so a single initializer would either need to accept many optional parameters or use an associated-value enum. Static factory methods are clearer and allow each version to have its own natural parameter list. ### Supporting all UUID versions immediately diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index f8daf20c5b..f6676b677c 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -302,12 +302,18 @@ extension UUID { Int(_storage[6] >> 4) } + /// Creates a new UUID with RFC 9562 version 4 (random) layout. This is equivalent to calling `UUID()`. + @export(implementation) + public static func version4() -> UUID { + UUID() + } + /// Creates a new UUID with RFC 9562 version 7 layout: a Unix timestamp in milliseconds in the most significant 48 bits, followed by random bits. The variant and version fields are set per the RFC. /// /// Version 7 UUIDs sort in chronological order when compared using the standard `<` operator, making them well-suited as database primary keys. UUIDs generated within the same process are guaranteed to be monotonically increasing. - public static func timeOrdered() -> UUID { + public static func version7() -> UUID { var generator = SystemRandomNumberGenerator() - return timeOrdered(using: &generator) + return version7(using: &generator) } /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits. @@ -318,7 +324,7 @@ extension UUID { /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. /// - Returns: A version 7 UUID. - public static func timeOrdered( + public static func version7( using generator: inout some RandomNumberGenerator, at date: Date? = nil, offset: Duration = .zero diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 0e1b12250f..6ba28ac8bd 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -299,41 +299,41 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func timeOrderedVersionAndVariant() { + @Test func version7VersionAndVariant() { for _ in 0..<10000 { - let uuid = UUID.timeOrdered() + let uuid = UUID.version7() #expect(uuid.version == 7) #expect(uuid.variant == 0b10) } } @available(FoundationPreview 6.4, *) - @Test func timeOrderedUsingGeneratorVersionAndVariant() { + @Test func version7UsingGeneratorVersionAndVariant() { var generator = SystemRandomNumberGenerator() for _ in 0..<10000 { - let uuid = UUID.timeOrdered(using: &generator) + let uuid = UUID.version7(using: &generator) #expect(uuid.version == 7) #expect(uuid.variant == 0b10) } } @available(FoundationPreview 6.4, *) - @Test func timeOrderedUsingGeneratorTimestamp() throws { + @Test func version7UsingGeneratorTimestamp() throws { var generator = SystemRandomNumberGenerator() let date = Date(timeIntervalSince1970: 1700000000.123) - let uuid = UUID.timeOrdered(using: &generator, at: date) + let uuid = UUID.version7(using: &generator, at: date) let timestamp = try #require(uuid.date) // We will lose some precision from the original date in the encoded date. #expect(timestamp.timeIntervalSince1970.rounded(.down) == date.timeIntervalSince1970.rounded(.down)) } @available(FoundationPreview 6.4, *) - @Test func timeOrderedUsingDeterministicGenerator() { + @Test func version7UsingDeterministicGenerator() { let fixedDate = Date(timeIntervalSince1970: 1645557742.0) // RFC 9562 A.6 timestamp var gen1 = PCGRandomNumberGenerator(seed: 42) var gen2 = PCGRandomNumberGenerator(seed: 42) - let uuid1 = UUID.timeOrdered(using: &gen1, at: fixedDate) - let uuid2 = UUID.timeOrdered(using: &gen2, at: fixedDate) + let uuid1 = UUID.version7(using: &gen1, at: fixedDate) + let uuid2 = UUID.version7(using: &gen2, at: fixedDate) // Same seed and same date produces identical UUIDs #expect(uuid1 == uuid2) // Verify the timestamp round-trips @@ -341,12 +341,12 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func timeOrderedDifferentSeedsSameDate() { + @Test func version7DifferentSeedsSameDate() { let fixedDate = Date(timeIntervalSince1970: 1645557742.0) var gen1 = PCGRandomNumberGenerator(seed: 42) var gen2 = PCGRandomNumberGenerator(seed: 99) - let uuid1 = UUID.timeOrdered(using: &gen1, at: fixedDate) - let uuid2 = UUID.timeOrdered(using: &gen2, at: fixedDate) + let uuid1 = UUID.version7(using: &gen1, at: fixedDate) + let uuid2 = UUID.version7(using: &gen2, at: fixedDate) // Same date but different seeds produces different UUIDs #expect(uuid1 != uuid2) // Both should still have the same timestamp @@ -354,22 +354,22 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func timeOrderedAtSpecificDate() throws { + @Test func version7AtSpecificDate() throws { let date = Date(timeIntervalSince1970: 1000.0) var generator = SystemRandomNumberGenerator() - let uuid = UUID.timeOrdered(using: &generator, at: date) + let uuid = UUID.version7(using: &generator, at: date) let timestamp = try #require(uuid.date) #expect(timestamp == date) #expect(uuid.version == 7) } @available(FoundationPreview 6.4, *) - @Test func timeOrderedSubMillisecondPrecision() { + @Test func version7SubMillisecondPrecision() { // RFC 9562 Section 6.2 Method 3: rand_a encodes sub-ms precision. // Use a whole-millisecond date (no sub-ms fraction) to verify rand_a == 0 let date = Date(timeIntervalSince1970: 1000.123) var generator = SystemRandomNumberGenerator() - let uuid = UUID.timeOrdered(using: &generator, at: date) + let uuid = UUID.version7(using: &generator, at: date) // rand_a is the lower nibble of byte 6 and all of byte 7 let randA = (UInt16(uuid.span[6]) & 0x0F) << 8 | UInt16(uuid.span[7]) #expect(randA == 0) @@ -378,33 +378,33 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func timeOrderedWithOffsetFromDate() throws { + @Test func version7WithOffsetFromDate() throws { let base = Date(timeIntervalSince1970: 1000.0) let offset = Duration.seconds(60) var generator = SystemRandomNumberGenerator() - let uuid = UUID.timeOrdered(using: &generator, at: base, offset: offset) + let uuid = UUID.version7(using: &generator, at: base, offset: offset) let timestamp = try #require(uuid.date) // Should encode base + 60s = 1060.0 #expect(timestamp == Date(timeIntervalSince1970: 1060.0)) } @available(FoundationPreview 6.4, *) - @Test func timeOrderedWithNegativeOffset() throws { + @Test func version7WithNegativeOffset() throws { let base = Date(timeIntervalSince1970: 2000.0) let offset = Duration.seconds(-500) var generator = SystemRandomNumberGenerator() - let uuid = UUID.timeOrdered(using: &generator, at: base, offset: offset) + let uuid = UUID.version7(using: &generator, at: base, offset: offset) let timestamp = try #require(uuid.date) // Should encode base - 500s = 1500.0 #expect(timestamp == Date(timeIntervalSince1970: 1500.0)) } @available(FoundationPreview 6.4, *) - @Test func timeOrderedWithOffsetFromCurrentTime() { + @Test func version7WithOffsetFromCurrentTime() { // Offset of +1 hour from current time should produce a UUID with a timestamp roughly 1 hour in the future let before = Date().addingTimeInterval(3600.0 - 1.0) var generator = SystemRandomNumberGenerator() - let uuid = UUID.timeOrdered(using: &generator, offset: .seconds(3600)) + let uuid = UUID.version7(using: &generator, offset: .seconds(3600)) let timestamp = uuid.date! let after = Date().addingTimeInterval(3600.0 + 1.0) #expect(timestamp >= before) @@ -412,22 +412,22 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func timeOrderedMonotonicity() { + @Test func version7Monotonicity() { // Generate many UUIDs in a tight loop without any delays. // The monotonic guarantee ensures each is strictly greater than the previous, even within the same sub-millisecond. - var previous = UUID.timeOrdered() + var previous = UUID.version7() for _ in 0..<10_000 { - let current = UUID.timeOrdered() + let current = UUID.version7() #expect(previous < current) previous = current } } @available(FoundationPreview 6.4, *) - @Test func timeOrderedDate() throws { + @Test func version7Date() throws { let date = Date(timeIntervalSince1970: 1700000000.456) var generator = SystemRandomNumberGenerator() - let uuid = UUID.timeOrdered(using: &generator, at: date) + let uuid = UUID.version7(using: &generator, at: date) let timestamp = try #require(uuid.date) #expect(timestamp == date) } @@ -435,7 +435,7 @@ private struct UUIDTests { // RFC 9562 Appendix A.6: UUIDv7 test vector with known timestamp // Tuesday, February 22, 2022 2:22:22.00 PM GMT-05:00 = 1645557742000 ms @available(FoundationPreview 6.4, *) - @Test func timeOrderedDateRFCVector() throws { + @Test func version7DateRFCVector() throws { let v7 = UUID(uuidString: "017F22E2-79B0-7CC3-98C4-DC0C0C07398F")! let timestamp = try #require(v7.date) let expected = Date(timeIntervalSince1970: 1645557742.0) @@ -443,7 +443,7 @@ private struct UUIDTests { } @available(FoundationPreview 6.4, *) - @Test func timeOrderedDateNilForV4() { + @Test func version7DateNilForV4() { let uuid = UUID() #expect(uuid.date == nil) } From 2feabf7b442a0e6ba8632badc2943653872c1f3e Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Mon, 23 Mar 2026 13:18:13 -0700 Subject: [PATCH 12/15] Add section on Clock to alternatives considered --- Proposals/NNNN-uuid-versions.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index b9f88977b7..90002d0ba7 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -223,3 +223,7 @@ Instead of `UUID.version7()`, we considered `UUID(version: 7)`. However, differe ### Supporting all UUID versions immediately We considered adding factory methods for all versions (1, 3, 5, 6, 7, 8), but the immediate need is version 7. Version 1 (time-based with MAC address) has privacy implications. Versions 3 and 5 require different parameters. Version 6 is a reordering of version 1 and shares its concerns. Version 8 is intentionally application-defined. Starting with version 7 keeps the proposal focused. + +### Accepting a `Clock` parameter instead of `Date` + +We considered accepting a `Clock` argument to allow callers to inject a custom time source. However, RFC 9562 requires the timestamp to represent [Unix time](https://en.wikipedia.org/wiki/Unix_time) — specifically, the number of milliseconds since the Unix epoch (1 January 1970 UTC). This corresponds to what Swift would call a `UTCClock` (see the [UTCClock pitch](https://forums.swift.org/t/pitch-utcclock/78018)), not an arbitrary clock. A `SuspendingClock` or `ContinuousClock` measures elapsed time since boot, which would produce an incorrect UUID timestamp. Any clock that *does* produce correct results would necessarily be equivalent to `UTCClock`, making the generality unnecessary. Instead, we accept an optional `Date` for callers who need to embed a specific point in time. This matches the convention used across Foundation for representations of time since the Unix epoch. From 3018a019212334c91be5822f1d8b015dfdc536a8 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Mon, 30 Mar 2026 14:19:51 -0700 Subject: [PATCH 13/15] Add some alternatives considered from the pitch thread --- Proposals/NNNN-uuid-versions.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index 90002d0ba7..d2521fa811 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -220,10 +220,28 @@ This feature can be freely adopted and un-adopted in source code with no deploym Instead of `UUID.version7()`, we considered `UUID(version: 7)`. However, different versions require different parameters — version 5 needs a name and namespace, version 8 needs custom data — so a single initializer would either need to accept many optional parameters or use an associated-value enum. Static factory methods are clearer and allow each version to have its own natural parameter list. +We also considered using an enumeration for each version with associated types for the different parameters. In practice, this doesn't look or act much differently than simply adding functions to `UUID` with the required arguments. + ### Supporting all UUID versions immediately We considered adding factory methods for all versions (1, 3, 5, 6, 7, 8), but the immediate need is version 7. Version 1 (time-based with MAC address) has privacy implications. Versions 3 and 5 require different parameters. Version 6 is a reordering of version 1 and shares its concerns. Version 8 is intentionally application-defined. Starting with version 7 keeps the proposal focused. +### Different types for different versions + +We considered adding different types for each version of a UUID. Community feedback suggests that it is rare to need to restrict version at a _type_ level. If this functionality is needed, the `version` property can be checked dynamically at runtime. + ### Accepting a `Clock` parameter instead of `Date` We considered accepting a `Clock` argument to allow callers to inject a custom time source. However, RFC 9562 requires the timestamp to represent [Unix time](https://en.wikipedia.org/wiki/Unix_time) — specifically, the number of milliseconds since the Unix epoch (1 January 1970 UTC). This corresponds to what Swift would call a `UTCClock` (see the [UTCClock pitch](https://forums.swift.org/t/pitch-utcclock/78018)), not an arbitrary clock. A `SuspendingClock` or `ContinuousClock` measures elapsed time since boot, which would produce an incorrect UUID timestamp. Any clock that *does* produce correct results would necessarily be equivalent to `UTCClock`, making the generality unnecessary. Instead, we accept an optional `Date` for callers who need to embed a specific point in time. This matches the convention used across Foundation for representations of time since the Unix epoch. + +### Static function names + +Many contributors suggested the use of shorter names like `v7`. While this is unlikely to be confusing to readers, the short name feels overly informal. The Swift API guidelines also suggest avoiding abbreviations. + +We considered prefixing the function name with `make`, as the Swift naming guidelines suggest for some factory methods. However, this pattern is actually rare for similar Foundation API. Similar unprefixed API include `Date.now`, `RecurrenceRule` constructors like `.hourly(...)`, `.monthly(...)`, `CocoaError.error(...)`, `URL.temporaryDirectory`, `URL.homeDirectory(forUser: ...)` and more. + +### Deprecating `UUID()` + +We considered deprecating the no-argument initializer for `UUID`. We believe that this could be counter-productive in the long term, because it can create "deprecation fatigue." This may encourage callers to ignore warnings because they feel somewhat arbitrary, especially when existing code is correct and will continue to work in the future. + +For similar reasons, we cannot change the behavior of the current methods to change the case of the string or version of the result. For example, we expect there to be existing code that would break if we change the result of the `uuidString` to be lowercased. From 47c2b3e197a37fc37652c47083eaa6b6094d5db7 Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Tue, 31 Mar 2026 09:43:40 -0700 Subject: [PATCH 14/15] Add version setter, variant property --- Proposals/NNNN-uuid-versions.md | 26 ++++++++-- Sources/FoundationEssentials/UUID.swift | 36 +++++++++++++- .../FoundationEssentialsTests/UUIDTests.swift | 49 +++++++++++++++---- 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index d2521fa811..ea74e1d550 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -138,17 +138,37 @@ let uuid = UUID { output in The closure receives an `OutputSpan` backed by the UUID's 16-byte storage. If the closure writes fewer or more than 16 bytes, the initializer traps. If the closure throws, the error is propagated with its original type. -### `version` property +### `version` and `variant` properties ```swift @available(FoundationPreview 6.4, *) extension UUID { + /// The variant of a UUID, as defined by RFC 9562 Section 4.1. + public enum Variant: Sendable, Hashable { + /// NCS backward compatibility (variant bits `0xx`). + case ncs + + /// The variant specified by RFC 9562 (variant bits `10x`). + case rfc9562 + + /// Microsoft backward compatibility (variant bits `110`). + case microsoft + + /// Reserved for future use (variant bits `111`). + case reserved + } + /// The version of this UUID, derived from the version bits (bits 48–51) as defined by RFC 9562. - public var version: Int { get } + public var version: Int { get set } + + /// The variant of this UUID, derived from the variant bits (bits 64–65) as defined by RFC 9562. + public var variant: Variant { get } } ``` -The version value is encoded in bits 48–51 of the UUID (the high nibble of byte 6), per RFC 9562. The returned `Int` ranges from 0 to 15. Well-known versions include 1 (time-based), 3 (name-based MD5), 4 (random), 5 (name-based SHA-1), 6 (reordered time-based), 7 (time-ordered), and 8 (custom). +The version value is encoded in bits 48–51 of the UUID (the high nibble of byte 6), per RFC 9562. The returned `Int` ranges from 0 to 15. Well-known versions include 1 (time-based), 3 (name-based MD5), 4 (random), 5 (name-based SHA-1), 6 (reordered time-based), 7 (time-ordered), and 8 (custom). The setter replaces only the version nibble, preserving all other bits. + +The variant value is encoded in the high bits of byte 8 (bits 64–65). UUIDs created by Foundation use the RFC 9562 variant (`.rfc9562`, binary `10`). The `Variant` enum covers all four variant values defined by RFC 9562. ### Creating version 4 and version 7 UUIDs diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index f6676b677c..90f805b078 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -299,7 +299,41 @@ extension UUID { extension UUID { /// The version of this UUID, derived from the version bits (bits 48–51) as defined by RFC 9562. public var version: Int { - Int(_storage[6] >> 4) + get { + Int(_storage[6] >> 4) + } + set { + _storage[6] = (_storage[6] & 0x0F) | (UInt8(newValue & 0x0F) << 4) + } + } + + /// The variant of a UUID, as defined by RFC 9562 Section 4.1. + public enum Variant: Sendable, Hashable { + /// NCS backward compatibility (variant bits `0xx`). + case ncs + + /// The variant specified by RFC 9562 (variant bits `10x`). + case rfc9562 + + /// Microsoft backward compatibility (variant bits `110`). + case microsoft + + /// Reserved for future use (variant bits `111`). + case reserved + } + + /// The variant of this UUID, derived from the variant bits (bits 64–65) as defined by RFC 9562. + public var variant: Variant { + let byte = _storage[8] + if byte & 0x80 == 0 { + return .ncs + } else if byte & 0xC0 == 0x80 { + return .rfc9562 + } else if byte & 0xE0 == 0xC0 { + return .microsoft + } else { + return .reserved + } } /// Creates a new UUID with RFC 9562 version 4 (random) layout. This is equivalent to calling `UUID()`. diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 6ba28ac8bd..7e1e68fabf 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -147,7 +147,7 @@ private struct UUIDTests { for _ in 0..<10000 { let uuid = UUID.random(using: &generator) #expect(uuid.version == 0b0100) - #expect(uuid.variant == 0b10) + #expect(uuid.variant == .rfc9562) } } @@ -303,7 +303,7 @@ private struct UUIDTests { for _ in 0..<10000 { let uuid = UUID.version7() #expect(uuid.version == 7) - #expect(uuid.variant == 0b10) + #expect(uuid.variant == .rfc9562) } } @@ -313,7 +313,7 @@ private struct UUIDTests { for _ in 0..<10000 { let uuid = UUID.version7(using: &generator) #expect(uuid.version == 7) - #expect(uuid.variant == 0b10) + #expect(uuid.variant == .rfc9562) } } @@ -458,6 +458,43 @@ private struct UUIDTests { } } + @available(FoundationPreview 6.4, *) + @Test func versionSetter() { + var uuid = UUID() + #expect(uuid.version == 4) + uuid.version = 7 + #expect(uuid.version == 7) + // The lower nibble of byte 6 should be preserved + let original = UUID() + var modified = original + modified.version = 7 + #expect(modified.span[6] & 0x0F == original.span[6] & 0x0F) + #expect(modified.version == 7) + } + + @available(FoundationPreview 6.4, *) + @Test func variantProperty() { + // v4 UUIDs have RFC 9562 variant + let uuid = UUID() + #expect(uuid.variant == .rfc9562) + + // v7 UUIDs have RFC 9562 variant + let v7 = UUID.version7() + #expect(v7.variant == .rfc9562) + + // Manually constructed UUID with NCS variant (top bit 0) + let ncs = UUID(uuid: (0, 0, 0, 0, 0, 0, 0x40, 0, 0x00, 0, 0, 0, 0, 0, 0, 0)) + #expect(ncs.variant == .ncs) + + // Microsoft variant (top 3 bits 110) + let ms = UUID(uuid: (0, 0, 0, 0, 0, 0, 0x40, 0, 0xC0, 0, 0, 0, 0, 0, 0, 0)) + #expect(ms.variant == .microsoft) + + // Reserved variant (top 3 bits 111) + let res = UUID(uuid: (0, 0, 0, 0, 0, 0, 0x40, 0, 0xE0, 0, 0, 0, 0, 0, 0, 0)) + #expect(res.variant == .reserved) + } + @available(FoundationPreview 6.3, *) @Test func deterministicRandomGeneration() { var generator = PCGRandomNumberGenerator(seed: 123456789) @@ -476,12 +513,6 @@ private struct UUIDTests { } } -extension UUID { - fileprivate var variant: Int { - Int(self.uuid.8 >> 6 & 0b11) - } -} - /// A seedable random number generator for deterministic testing. The same seed always produces the same sequence, allowing tests to verify exact UUID output. fileprivate struct PCGRandomNumberGenerator: RandomNumberGenerator { private static let multiplier: UInt128 = 47_026_247_687_942_121_848_144_207_491_837_523_525 From 872a24d49c651f2a7f7664dea6ea73eb6319f70b Mon Sep 17 00:00:00 2001 From: Tony Parker Date: Wed, 1 Apr 2026 11:00:18 -0700 Subject: [PATCH 15/15] Allow specifying the date and offset with the version7 function that does not also take a generator --- Proposals/NNNN-uuid-versions.md | 5 ++++- Sources/FoundationEssentials/UUID.swift | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md index ea74e1d550..b63f0b84c9 100644 --- a/Proposals/NNNN-uuid-versions.md +++ b/Proposals/NNNN-uuid-versions.md @@ -181,7 +181,10 @@ extension UUID { /// Creates a new UUID with RFC 9562 version 7 layout: a Unix timestamp in milliseconds in the most significant 48 bits, followed by random bits. The variant and version fields are set per the RFC. /// /// Version 7 UUIDs sort in chronological order when compared using the standard `<` operator, making them well-suited as database primary keys. UUIDs generated within the same process are guaranteed to be monotonically increasing. - public static func version7() -> UUID + /// + /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. + /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. + public static func version7(at date: Date? = nil, offset: Duration = .zero) -> UUID /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits. /// diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 90f805b078..20d03289b5 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -345,9 +345,12 @@ extension UUID { /// Creates a new UUID with RFC 9562 version 7 layout: a Unix timestamp in milliseconds in the most significant 48 bits, followed by random bits. The variant and version fields are set per the RFC. /// /// Version 7 UUIDs sort in chronological order when compared using the standard `<` operator, making them well-suited as database primary keys. UUIDs generated within the same process are guaranteed to be monotonically increasing. - public static func version7() -> UUID { + /// + /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. + /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. + public static func version7(at date: Date? = nil, offset: Duration = .zero) -> UUID { var generator = SystemRandomNumberGenerator() - return version7(using: &generator) + return version7(using: &generator, at: date, offset: offset) } /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits.