diff --git a/Sources/SortedCollections/CaseInsensitiveStringSet/CaseInsensitiveStringSet+Conformances.swift b/Sources/SortedCollections/CaseInsensitiveStringSet/CaseInsensitiveStringSet+Conformances.swift new file mode 100644 index 000000000..638fa1ea2 --- /dev/null +++ b/Sources/SortedCollections/CaseInsensitiveStringSet/CaseInsensitiveStringSet+Conformances.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension CaseInsensitiveStringSet: CustomDebugStringConvertible { + public var debugDescription: String { + String(describing: Self.self) + + "([\(self.lazy.map(String.init(reflecting:)).joined(separator: ", "))])" + } +} + +extension CaseInsensitiveStringSet: CustomReflectable { + public var customMirror: Mirror { + Mirror(self, unlabeledChildren: Array(self), displayStyle: .set) + } +} + +extension CaseInsensitiveStringSet: CustomStringConvertible { + public var description: String { + "[\(self.lazy.map({ $0.folding(options: .caseInsensitive, locale: nil) }).map(String.init(reflecting:)).joined(separator: ", "))]" + } +} + +extension CaseInsensitiveStringSet: Decodable { + public init(from decoder: any Decoder) throws { + var container = try decoder.unkeyedContainer() + var strings = [String]() + strings.reserveCapacity(container.count ?? 0) + while !container.isAtEnd { + strings.append(try container.decode(String.self)) + } + self.init(strings) + } +} + +extension CaseInsensitiveStringSet: Encodable { + public func encode(to encoder: any Encoder) throws { + var container = encoder.unkeyedContainer() + try container.encode(contentsOf: self) + } +} + +extension CaseInsensitiveStringSet: Hashable { + public func hash(into hasher: inout Hasher) { + for element in self { + var folded = element.folding(options: .caseInsensitive, locale: nil) + folded = folded.decomposedStringWithCanonicalMapping + folded.withUTF8 { hasher.combine(bytes: UnsafeRawBufferPointer($0)) } + } + } +} diff --git a/Sources/SortedCollections/CaseInsensitiveStringSet/CaseInsensitiveStringSet+Miscellaneous.swift b/Sources/SortedCollections/CaseInsensitiveStringSet/CaseInsensitiveStringSet+Miscellaneous.swift new file mode 100644 index 000000000..4471063b5 --- /dev/null +++ b/Sources/SortedCollections/CaseInsensitiveStringSet/CaseInsensitiveStringSet+Miscellaneous.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +extension CaseInsensitiveStringSet { + /// The number of elements in the set. + /// + /// - Complexity: `O(1)`. + public var count: Int { self.inner.count } + + /// The first element of the set. + /// + /// If this set is empty, the value of this property is `nil`. + public var first: Element? { self.inner.first } + + /// Removes and returns the first element of the set. + /// + /// - Returns: The first element of this set if the set is not empty; + /// otherwise, `nil`. + /// + /// - Complexity: `O(log n)`, where *n* is the length of this set. + public mutating func popFirst() -> Element? { + return self.inner.popFirst() + } + + /// Removes all elements from the set. + public mutating func removeAll() { + self.inner.removeAll() + } + /// Removes and returns the least-ranked element of the set. + /// + /// The set must not be empty. + /// + /// - Returns: The removed element. + /// + /// - Complexity: `O(log n)`, where *n* is the length of this set. + @discardableResult + public mutating func removeFirst() -> Element { + return self.inner.removeFirst() + } + /// Removes the specified number of the least-ranked elements from the set. + /// + /// - Parameter k: The number of elements to remove from the set. + /// `k` must be greater than or equal to zero and must not exceed the + /// number of elements in the set. + /// + /// - Complexity: `O(k × log n)`, where *n* is the length of this set. + public mutating func removeFirst(_ k: Int) { + self.inner.removeFirst(k) + } +} diff --git a/Sources/SortedCollections/CaseInsensitiveStringSet/CaseInsensitiveStringSet.swift b/Sources/SortedCollections/CaseInsensitiveStringSet/CaseInsensitiveStringSet.swift new file mode 100644 index 000000000..9b0128052 --- /dev/null +++ b/Sources/SortedCollections/CaseInsensitiveStringSet/CaseInsensitiveStringSet.swift @@ -0,0 +1,224 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +/** + Case-insensitive ordered set of `String`. + + This file defines `CaseInsensitiveStringSet`, + a lightweight wrapper around `SkipListedSortedSet` that compares and + stores `String` values using a case-insensitive collation. + Membership, ordering, and set operations all use case-insensitive comparisons. + For example, the strings "apple" and "APPLE" are considered equivalent and + the set will contain at most one of them. + + The underlying storage is a `SortedSet` parameterized by a custom `Orderable` + implementation that performs localized, + case-insensitive comparisons with `String.compare(_:options:locale:)` using + the `.caseInsensitive` option. + + ### Examples + ```swift + var set = CaseInsensitiveStringSet() + set.insert("apple") + set.insert("APPLE") // Not added as a distinct element + set.insert("Banana") + + // Membership is case-insensitive + set.contains("apple") // true + set.contains("APPLE") // true + set.contains("banana") // true + set.contains("BANANA") // true + + // Iteration yields case-insensitive ascending order + // ["apple", "Banana"] (actual stored casing depends on first insertion) + let elements = Array(set) + ``` + */ + +import Foundation + +/// A set of unique `String` values compared case-insensitively and kept in +/// sorted order. +/// +/// `CaseInsensitiveStringSet` behaves like a regular set, +/// but all equality and ordering checks are performed without regard to +/// letter case. +/// This means inserting any casing variant of an existing element will not +/// increase the set's count, +/// and iteration yields elements in case-insensitive ascending order. +/// +/// ### Examples +/// ```swift +/// // Create from a sequence +/// let s1 = CaseInsensitiveStringSet(["a", "B", "b"]) // contains "a", "B" +/// +/// // Create from an array literal +/// let s2: CaseInsensitiveStringSet = ["Hello", "WORLD", "world"] +/// // s2.count == 2 +/// +/// // Insertion returns whether a new element was inserted +/// var s3: CaseInsensitiveStringSet = [] +/// let result1 = s3.insert("Swift") +/// result1.inserted // true +/// let result2 = s3.insert("swift") +/// result2.inserted // false (equivalent element already present) +/// +/// // Set algebra operations +/// let a: CaseInsensitiveStringSet = ["red", "GREEN"] +/// let b: CaseInsensitiveStringSet = ["Green", "BLUE"] +/// let u = a.union(b) // ["BLUE", "GREEN", "red"] +/// let i = a.intersection(b) // ["GREEN"] +/// let d = a.subtracting(b) // ["red"] +/// let x = a.symmetricDifference(b) // ["BLUE", "red"] +/// ``` +public struct CaseInsensitiveStringSet { + /// The element type stored by the set. Always `String`. + public typealias Element = _Ordering.Element + + /// Creates a set by wrapping an existing sorted-set implementation. + /// + /// - Parameter implementation: The underlying storage configured with the + /// case-insensitive ordering used by this type. + /// + /// - Postcondition: This set will have the same elements as `implementation`. + /// - Note: This initializer is internal and primarily intended for bridging + /// with the underlying `SortedSet`. + init(wrapping implementation: _Inner) { + self.inner = implementation + } + + /// The underlying storage type. + public typealias _Inner = SkipListedSortedSet<_Ordering> + + /// The wrapped storage instance implementing all set semantics. + var inner: _Inner + + /// Case-insensitive ordering for `String` elements. + /// + /// This `Orderable` implementation defines a total order and equivalence that + /// both use case-insensitive string comparison. + public enum _Ordering: Orderable { + public static func areDecreasing(_ lhs: Element, _ rhs: Element) -> Bool { + lhs.compare(rhs, options: .caseInsensitive, locale: nil) + == .orderedDescending + } + + public static func areEquivalent(_ lhs: Element, _ rhs: Element) -> Bool { + lhs.compare(rhs, options: .caseInsensitive, locale: nil) == .orderedSame + } + + public static func areIncreasing(_ lhs: Element, _ rhs: Element) -> Bool { + lhs.compare(rhs, options: .caseInsensitive, locale: nil) + == .orderedAscending + } + + public typealias Element = String + } +} + +extension CaseInsensitiveStringSet: Comparable, Sequence, SetAlgebra { + public static func < (lhs: Self, rhs: Self) -> Bool { + return lhs.inner < rhs.inner + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.inner == rhs.inner + } + + public func contains(_ member: Element) -> Bool { + return self.inner.contains(member) + } + + public mutating func formIntersection(_ other: Self) { + self.inner.formIntersection(other.inner) + } + + public mutating func formSymmetricDifference(_ other: __owned Self) { + self.inner.formSymmetricDifference(other.inner) + } + + public mutating func formUnion(_ other: __owned Self) { + self.inner.formUnion(other.inner) + } + + public init() { + self.init(wrapping: .init()) + } + + public init(arrayLiteral elements: Element...) { + self.init(elements) + } + + public init(_ sequence: __owned some Sequence) { + self.init(wrapping: .init(sequence)) + } + + @discardableResult + public mutating func insert(_ newMember: __owned Element) -> ( + inserted: Bool, memberAfterInsert: Element + ) { + return self.inner.insert(newMember) + } + + public func intersection(_ other: Self) -> Self { + return Self(wrapping: self.inner.intersection(other.inner)) + } + + public func isDisjoint(with other: Self) -> Bool { + return self.inner.isDisjoint(with: other.inner) + } + + public var isEmpty: Bool { inner.isEmpty } + + public func isSubset(of other: Self) -> Bool { + return self.inner.isSubset(of: other.inner) + } + + public func isSuperset(of other: Self) -> Bool { + return self.inner.isSuperset(of: other.inner) + } + + public typealias Iterator = _Inner.Iterator + + public func makeIterator() -> Iterator { + return inner.makeIterator() + } + + @discardableResult + public mutating func remove(_ member: Element) -> Element? { + return self.inner.remove(member) + } + + public mutating func subtract(_ other: Self) { + self.inner.subtract(other.inner) + } + + public func subtracting(_ other: Self) -> Self { + return Self(wrapping: self.inner.subtracting(other.inner)) + } + + public func symmetricDifference(_ other: __owned Self) -> Self { + return Self(wrapping: self.inner.symmetricDifference(other.inner)) + } + + public var underestimatedCount: Int { inner.underestimatedCount } + + public func union(_ other: __owned Self) -> Self { + return Self(wrapping: self.inner.union(other.inner)) + } + + @discardableResult + public mutating func update(with newMember: __owned Element) -> Element? { + return self.inner.update(with: newMember) + } +} diff --git a/Sources/SortedCollections/Orderable.swift b/Sources/SortedCollections/Orderable.swift new file mode 100644 index 000000000..81270a7a2 --- /dev/null +++ b/Sources/SortedCollections/Orderable.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +/** + A generic protocol that models a total preorder for elements. + Types conforming to `Orderable` define a consistent way to compare two + values of the same `Element` type in terms of increasing, equivalent, + and decreasing relationships. + This can be used to parameterize sorting, ordered collections, and + algorithms that need a customizable notion of + order without relying on `Comparable`. + Conformers are expected to implement `areIncreasing(_: _:)` as a + strict ordering relation. + By default, `areEquivalent(_: _:)` and `areDecreasing(_: _:)` are derived + from `areIncreasing(_: _:)` to form a total preorder: exactly one of + increasing, equivalent, or decreasing holds for any pair of elements. + + Example: + + ```swift + // A simple ascending ordering for Int values + enum AscendingIntOrder: Orderable { + static func areIncreasing(_ lhs: Int, _ rhs: Int) -> Bool { lhs < rhs } + } + + let numbers = [5, 1, 3] + let sortedAscending = numbers.sorted { AscendingIntOrder.areIncreasing($0, + $1) } + // [1, 3, 5] + + // A reverse ordering can be defined by flipping the relation + enum DescendingIntOrder: Orderable { + static func areIncreasing(_ lhs: Int, _ rhs: Int) -> Bool { lhs > rhs } + } + + let sortedDescending = numbers.sorted { DescendingIntOrder.areIncreasing($0, + $1) } + // [5, 3, 1] + ``` + */ + +/// An abstraction over a customizable ordering relation for values of +/// type `Element`. +/// +/// Provide a strict "is increasing" relation via `areIncreasing(_: _:)`. +/// The default implementations derive equivalence and decreasing relations to +/// complete the ordering. +public protocol Orderable { + + /// The element type compared by this ordering. + associatedtype Element + + /// Returns `true` if `lhs` should come before `rhs` according to + /// this ordering. + /// + /// This relation should be strict and transitive. For any value `x`, + /// `areIncreasing(x, x)` should be `false`. + static func areIncreasing(_ lhs: Element, _ rhs: Element) -> Bool + + /// Returns `true` if `lhs` and `rhs` are considered equivalent under + /// this ordering. + /// + /// By default, this is derived from `areIncreasing` and `areDecreasing` so + /// that two elements are equivalent when neither precedes the other. + static func areEquivalent(_ lhs: Element, _ rhs: Element) -> Bool + + /// Returns `true` if `lhs` should come after `rhs` according to + /// this ordering. + /// + /// By default, this is implemented as `areIncreasing(rhs, lhs)`. + static func areDecreasing(_ lhs: Element, _ rhs: Element) -> Bool +} + +/// Default implementations derived from `areIncreasing(_: _:)`. +/// +/// Conformers typically only need to implement `areIncreasing(_: _:)`. +/// The extension defines `areEquivalent(_: _:)` and `areDecreasing(_: _:)` +/// in terms of it. +extension Orderable { + /// Two elements are equivalent when neither + /// is strictly increasing over the other. + static public func areEquivalent(_ lhs: Element, _ rhs: Element) -> Bool { + return !areIncreasing(lhs, rhs) && !areDecreasing(lhs, rhs) + } + + /// Decreasing is defined as the inverse of increasing. + static public func areDecreasing(_ lhs: Element, _ rhs: Element) -> Bool { + return areIncreasing(rhs, lhs) + } +} diff --git a/Sources/SortedCollections/SkipList.swift b/Sources/SortedCollections/SkipList.swift new file mode 100644 index 000000000..caae7d759 --- /dev/null +++ b/Sources/SortedCollections/SkipList.swift @@ -0,0 +1,525 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +/** + SkipList + ======= + + This file implements a deterministic Skip List data structure, + allowing efficient ordered insertion, deletion, and search operations with + logarithmic time complexity on average. + + The implementation is adapted from "Skip Lists and Probabilistic Analysis of + Algorithms" by Thomas Papadakis (1993) and designed for generic use with + orderable elements. + */ + +/// A sorted collection supporting fast insertion, deletion, and +/// search operations. +/// +/// - Parameter TotalOrdering: Protocol defining the element type and a +/// total ordering for said element type. +struct SkipList: Sequence { + /// Ensures that the underlying storage is uniquely referenced by this list. + mutating func _ensureUnique() { + if !isKnownUniquelyReferenced(&_core) { + _core = Core(cloning: _core) + } + } + + /// The actual skip list structure with the stored values. + var _core: Core + + /// The core implementation of the skip list, + /// maintaining the structure and its operations. + final class Core { + /// Bottom-most sentinel node for skip list traversal. + var bottom = Node(.maximum) + /// Number of elements stored in the skip list. + private(set) var count = 0 + + /// Deletes an element from the skip list. + /// + /// - Parameter target: The element to delete. + /// - Returns: The removed element if it existed, or `nil`. + /// - Postcondition: The list has no elements equivalent to `target`. + func delete(_ target: Element) -> Element? { + let extendedTarget = ExtendedElement.normal(value: .init(target)) + let oldBottomValue = self.bottom.value + self.bottom.value = extendedTarget + defer { self.bottom.value = oldBottomValue } + + var outgoingValue: Element? + var precedingValue: ExtendedElement! + self.count -= 1 + do { + var pointer = self.head.below + var aboveValue = self.head.value + var afterPointer: Node! + while pointer !== self.bottom { + let previousPointer: Node! + let belowPointer: Node + (previousPointer, pointer) = pointer.linkedNodes(bracketing: target) + belowPointer = pointer.below + defer { + aboveValue = pointer.value + pointer = belowPointer + } + + if pointer.value == belowPointer.forward.value { + if pointer.value != aboveValue { + afterPointer = pointer.forward + + let belowAfterPointer = afterPointer.below + let afterBelowAfterPointer = belowAfterPointer.forward + if afterPointer.value == afterBelowAfterPointer.value + || belowPointer === self.bottom + { + pointer._forward = afterPointer.forward + pointer.value = afterPointer.value + + // Hope this is right + outgoingValue = outgoingValue ?? afterPointer.finiteValue + } else { + pointer.value = belowAfterPointer.value + afterPointer._below = afterBelowAfterPointer + } + } else { + let belowPreviousPointer = previousPointer.below + let oneAfterBelowPreviousPointer = belowPreviousPointer.forward + if previousPointer.value <= oneAfterBelowPreviousPointer.value { + if belowPointer === self.bottom { + precedingValue = previousPointer.value + } + previousPointer._forward = pointer.forward + previousPointer.value = pointer.value + + // Hope this is right + outgoingValue = outgoingValue ?? pointer.finiteValue + pointer = previousPointer + } else { + let twoAfterBelowPreviousPointer = oneAfterBelowPreviousPointer + .forward + afterPointer = + previousPointer.value == twoAfterBelowPreviousPointer.value + ? oneAfterBelowPreviousPointer : twoAfterBelowPreviousPointer + previousPointer.value = afterPointer.value + pointer._below = afterPointer.forward + } + } + } else if belowPointer === self.bottom { + // The target value wasn't in the list to begin with! + self.count += 1 // undo the now-unnecessary deduction + } + } + } + + do { + var pointer = self.head.below + while pointer !== self.bottom { + defer { pointer = pointer.below } + + pointer = pointer.firstLinkedNode(atLeast: target) + if extendedTarget == pointer.value { + outgoingValue = outgoingValue ?? pointer.finiteValue + pointer.value = precedingValue + } + } + } + + if self.head.below.forward === tail { + // Pop a now-excessive top head node. + self.head = self.head.below + } + + return outgoingValue + } + + /// Removes all elements from the skip list, resetting its structure. + /// + /// - Postcondition: `count == 0`. + func deleteAll() { + self.bottom.value = .maximum + self.head._forward = self.tail + self.head._below = self.bottom + } + + /// Represents an entry in the skip list, + /// supporting normal, maximum, and super-maximum sentinels. + enum ExtendedElement { + /// Returns the finite element value if present, otherwise `nil`. + var finiteValue: SkipList.Element? { + switch self { + case .normal(value: let result): + result.sample + case .maximum, .superMaximum: + nil + } + } + + /// A conventional value. + case normal(value: Value) + /// A value greater than any conventional value. + case maximum + /// A value greater than all others. + case superMaximum + } + + /// Head node of the highest level in the skip list. + var head: Node + /// Creates an empty skip list. + /// + /// - Postcondition: `count == 0`. + init() { + self.head = Node(.maximum, forward: self.tail, below: self.bottom) + } + /// Creates a skip list copying the values of the given one. + convenience init(cloning other: Core) { + self.init() + + // Iterate to the bottom layer. + var pointer = other.head + while pointer !== other.bottom { + pointer = pointer.below + } + + // Iterate across the bottom layer. + if pointer.value == .maximum { + pointer = pointer.forward + } + while case .normal(let storedValue) = pointer.value { + _ = self.insert(storedValue.sample) + pointer = pointer.forward + } + } + + /// Inserts an element into the skip list. + /// + /// - Parameter newValue: The element to insert. + /// - Returns: The previous element if it already existed, or `nil`. + /// - Postcondition: The list has one element equivalent to `newValue`. + func insert(_ newValue: Element) -> Element? { + let extendedNewValue = ExtendedElement.normal(value: .init(newValue)) + let oldBottomValue = self.bottom.value + self.bottom.value = extendedNewValue + defer { self.bottom.value = oldBottomValue } + + var old: Element? + self.count += 1 + do { + var pointer = self.head + while pointer !== self.bottom { + defer { pointer = pointer.below } + + pointer = pointer.firstLinkedNode(atLeast: newValue) + + let belowPointer = pointer.below + let oneAfterBelowPointer = belowPointer.forward + let twoAfterBelowPointer = oneAfterBelowPointer.forward + if pointer.value > twoAfterBelowPointer.value { + // Add a node for `value`. + // However, between that new node and its successor node in + // this layer, their projections in the immediately lower layer + // already have 3 nodes between them. + // Avoid violations by promoting one of those 3 nodes to + // this layer. + var newValueNode = Node( + pointer.value, + forward: pointer.forward, + below: twoAfterBelowPointer + ) + pointer._forward = newValueNode + pointer.value = oneAfterBelowPointer.value + } else if pointer.below === self.bottom { + // `pointer` must be at a node whose value already equals `value`. + assert(pointer.value == extendedNewValue) + guard case .normal(let normalValue) = extendedNewValue else { + preconditionFailure("This should not be reachable") + } + + old = normalValue.sample + self.count -= 1 // undo the now-unnecessary earlier `count += 1` + } + } + } + if self.head.forward !== self.tail { + let higherHead = Node(.maximum, forward: self.tail, below: self.head) + self.head = higherHead + } + + return old + } + + /// Represents a node within a skip list level, + /// linking forward and downward. + final class Node { + /// The node below this node at the next lower skip list level. + var _below: Node? + /// The node forward from this node at the same skip list level. + var _forward: Node? + /// The value associated with this node. + var value: ExtendedElement + + /// Returns the node at the same column as this one in the + /// layer immediately below this node's layer, + /// wrapping around to `self` if not present. + var below: Node { + self._below ?? self + } + + /// Returns the element value if not a maximum, otherwise `nil`. + var finiteValue: Element? { value.finiteValue } + + /// Returns the first linked node whose value is at least the target. + /// + /// - Parameter target: The element whose lower bound is sought. + /// - Returns: The first node with value >= target. + func firstLinkedNode(atLeast target: Element) -> Node { + return linkedNodes(bracketing: target).atOrAfter + } + + /// Returns the node after this node in this layer, + /// wrapping around to `self` if not present. + var forward: Node { + self._forward ?? self + } + + /// Creates a new node with the given value, + /// with pointers for the possible given neighbor nodes. + /// + /// - Parameters: + /// - value: The value stored in the node. + /// - forward: The node for the next higher value in this node's layer. + /// - below: This node's equivalent node in the next lower layer. + init(_ value: ExtendedElement, forward: Node? = nil, below: Node? = nil) { + self._below = below + self._forward = forward + self.value = value + } + + /// Finds nodes bracketing the target value. + /// + /// - Parameter target: The value to bracket. + /// - Returns: A pair of nodes, + /// where the second is the first node in this node's layer with + /// a value at least as much as `target`, + /// and the first is the node possible directly right before the second. + func linkedNodes(bracketing target: Element) -> ( + before: Node?, atOrAfter: Node + ) { + let extendedTarget = ExtendedElement.normal(value: .init(target)) + var previous: Node? + var present = self + while extendedTarget > present.value { + previous = present + present = present.forward + } + return (previous, present) + } + } + + /// Finds the element in this list that is equivalent to the given value, + /// optionally allowing the given value replace the currently stored one. + /// + /// - Parameters: + /// - target: The value to search for. + /// - doCanonize: Whether to replace the stored value with `target`. + /// If not given, no changes will occur. + /// - Returns: The equivalent node stored in this skip list, + /// before any possible replacement. + /// If there was no matching node, + /// `nil` is returned instead. + func representative(of target: Element, doCanonize: Bool = false) + -> Element? + { + var result = self.head + while result !== self.bottom { + defer { result = result.below } + + result = result.firstLinkedNode(atLeast: target) + guard result.below === self.bottom else { continue } + guard case .normal(value: let innerValue) = result.value, + TotalOrdering.areEquivalent(innerValue.sample, target) + else { + break + } + + defer { + if doCanonize { + innerValue.sample = target + } + } + return innerValue.sample + } + + return nil + } + + /// The tail-end sentinel node of this skip list structure. + var tail = Node(.superMaximum) + + /// Wraps a sample element, allowing reference-based storage and mutation. + final class Value { + /// The sample element value. + var sample: Element + + /// Creates a value wrapper around the given value. + init(_ sample: Element) { + self.sample = sample + } + } + } + + /// The number of elements contained in the skip list. + var count: Int { _core.count } + + /// Removes the specified element from the skip list. + /// + /// - Parameter target: The element to remove. + /// - Returns: The removed element if it was present, or `nil` if not found. + /// - Postcondition: This list will not have any elements equivalent to + /// `target`. + mutating func delete(_ target: Element) -> Element? { + self._ensureUnique() + return self._core.delete(target) + } + /// Removes all elements from the skip list, leaving it empty. + /// + /// - Postcondition: `count == 0`. + mutating func deleteAll() { + self._ensureUnique() + self._core.deleteAll() + } + + /// The type of the stored values. + typealias Element = TotalOrdering.Element + + /// Returns the canonical stored instance equivalent to the given element, if present. + /// + /// - Parameter target: The value to search for. + /// - Returns: The stored equivalent element, or `nil` if not found. + func getRepresentative(for target: Element) -> Element? { + return self._core.representative(of: target) + } + + init() { + self._core = .init() + } + + /// Inserts the given element into the skip list. + /// + /// - Parameter newValue: The value to insert. + /// - Returns: The existing stored element blocking change, + /// or `nil` if `newValue` was newly added. + mutating func insert(_ newValue: Element) -> Element? { + self._ensureUnique() + return self._core.insert(newValue) + } + + struct Iterator: IteratorProtocol { + /// The current node in the skip list iteration. + var current: Core.Node + + mutating func next() -> Element? { + defer { + current = current.forward + } + return current.finiteValue + } + } + + func makeIterator() -> Iterator { + // Iterate to the bottom layer. + var pointer = self._core.head + while pointer !== self._core.bottom { + pointer = pointer.below + } + + // Iterate from the bottom layer. + if pointer.value == .maximum { + pointer = pointer.forward + } + return .init(current: pointer) + } + + /// Canonicalizes the stored value equivalent to the given element, if present, replacing it with `target`. + /// + /// - Parameter target: The element whose equivalent is to be updated. + /// - Returns: The previous stored element if found, or `nil` if not present. + mutating func setRepresentative(for target: Element) -> Element? { + self._ensureUnique() + return self._core.representative(of: target, doCanonize: true) + } + + var underestimatedCount: Int { self.count } +} + +extension SkipList.Core.ExtendedElement: Comparable { + static func < (lhs: Self, rhs: Self) -> Bool { + return Self.compare(lhs, rhs) < 0 + } + static func == (lhs: Self, rhs: Self) -> Bool { + return Self.compare(lhs, rhs) == 0 + } + + static func > (lhs: Self, rhs: Self) -> Bool { + return Self.compare(lhs, rhs) > 0 + } + static func <= (lhs: Self, rhs: Self) -> Bool { + return Self.compare(lhs, rhs) <= 0 + } + static func >= (lhs: Self, rhs: Self) -> Bool { + return Self.compare(lhs, rhs) >= 0 + } + + /// Compares two extended elements according to skip list ordering rules. + /// + /// - Parameter lhs: The first (*i.e.* left) operand. + /// - Parameter rhs: The second (*i.e.* right) operand. + /// - Returns an integer giving the ordering relation between `lhs` and `rhs`, + /// given as equivalent to the relationship between the returned value + /// (on the left side) and `0` (on the right side). + static func compare(_ lhs: Self, _ rhs: Self) -> Int { + return switch (lhs, rhs) { + case (.normal(let l), .normal(let r)) + where TotalOrdering.areEquivalent(l.sample, r.sample): + fallthrough + case (.maximum, .maximum), (.superMaximum, .superMaximum): + 0 + case (.normal(let l), .normal(let r)) + where TotalOrdering.areIncreasing(l.sample, r.sample): + fallthrough + case (.normal, .maximum), (.normal, .superMaximum), + (.maximum, .superMaximum): + -1 + case (_, .normal), (.superMaximum, .maximum): + +1 + } + } +} + +extension SkipList.Core.ExtendedElement: CustomDebugStringConvertible { + /// A shorthand to calculate the containing type's name. + private static var typeName: String { .init(reflecting: Self.self) } + + var debugDescription: String { + switch self { + case .normal(let value): + Self.typeName + ".normal(\(String(reflecting: value.sample)))" + case .maximum: + Self.typeName + ".maximum" + case .superMaximum: + Self.typeName + ".superMaximum" + } + } +} diff --git a/Sources/SortedCollections/SkipListedSortedSet.swift b/Sources/SortedCollections/SkipListedSortedSet.swift new file mode 100644 index 000000000..72661c992 --- /dev/null +++ b/Sources/SortedCollections/SkipListedSortedSet.swift @@ -0,0 +1,258 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +/** + This file defines `SkipListedSortedSet`, + a set implementation based on a deterministic skip list structure designed for + efficient ordered set operations such as insertion, deletion, and + membership queries. + The skip list provides expected logarithmic time complexity for + these operations while maintaining elements in a sorted order according to + a specified total ordering. + */ + +/// An always-sorted set based on a skip list data structure. +/// +/// This type maintains elements in a sorted order as defined by +/// the provided `TotalOrdering`. +/// It supports efficient insertion, deletion, and searches with +/// the expected logarithmic time complexity. +/// Deterministic balencing is used to maintain performance. +public struct SkipListedSortedSet { + public init() {} + + /// The underlying skip list storage responsible for maintaining the + /// set's ordered elements efficiently. + var skipList = SkipList() +} + +// Operations inspired by `RangeReplaceableCollection`. +extension SkipListedSortedSet { + /// The number of elements in the set. + /// + /// - Complexity: `O(1)`. + public var count: Int { self.skipList.count } + + /// The first element of the set. + /// + /// If this set is empty, the value of this property is `nil`. + public var first: Element? { + var pointer = self.skipList._core.head + while pointer !== self.skipList._core.bottom { + pointer = pointer.below + } + if pointer.value == .maximum { + pointer = pointer.forward + } + return if case .normal(let result) = pointer.value { + result.sample + } else { + nil + } + } + + /// Removes and returns the first element of the set. + /// + /// - Returns: The first element of this set if the set is not empty; + /// otherwise, `nil`. + /// + /// - Complexity: `O(log n)`, where *n* is the length of this set. + mutating func popFirst() -> Element? { + return if let target = self.first { + self.remove(target) + } else { + nil + } + } + + /// Removes and returns the lowest-ranked element of the set. + /// + /// The set must not be empty. + /// + /// - Returns: The removed element. + /// + /// - Complexity: `O(log n)`, where *n* is the length of this set. + @discardableResult + public mutating func removeFirst() -> Element { + return self.popFirst()! + } + /// Removes the specified number of the lowest-ranked elements from the set. + /// + /// - Parameter k: The number of elements to remove from the set. + /// `k` must be greater than or equal to zero and must not exceed the + /// number of elements in the set. + /// + /// - Complexity: `O(k × log n)`, where *n* is the length of this set. + public mutating func removeFirst(_ k: Int) { + for _ in 0.. Bool { + return lhs.lexicographicallyPrecedes(rhs, by: TotalOrdering.areIncreasing) + } +} + +extension SkipListedSortedSet: Sequence { + public struct Iterator: IteratorProtocol { + var inner: SkipList.Iterator + + mutating public func next() -> TotalOrdering.Element? { + return inner.next() + } + } + + public func makeIterator() -> Iterator { + return .init(inner: self.skipList.makeIterator()) + } + + public var underestimatedCount: Int { self.skipList.underestimatedCount } +} + +extension SkipListedSortedSet: SetAlgebra { + // Use default implmentation of `init(arrayLiteral:)`. + + public static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.elementsEqual(rhs, by: TotalOrdering.areEquivalent) + } + + public func contains(_ member: Element) -> Bool { + self.skipList.getRepresentative(for: member) != nil + } + + public func union(_ other: __owned Self) -> Self { + return .init( + sortedMerge( + between: self, + and: other, + retaining: .union, + sortingBy: TotalOrdering.areIncreasing + ) + ) + } + + public func intersection(_ other: Self) -> Self { + return .init( + sortedMerge( + between: self, + and: other, + retaining: .intersection, + sortingBy: TotalOrdering.areIncreasing + ) + ) + } + + public func symmetricDifference(_ other: __owned Self) -> Self { + return .init( + sortedMerge( + between: self, + and: other, + retaining: .symmetricDifference, + sortingBy: TotalOrdering.areIncreasing + ) + ) + } + + public mutating func insert(_ newMember: __owned Element) -> ( + inserted: Bool, memberAfterInsert: Element + ) { + return if let old = self.skipList.insert(newMember) { + (inserted: false, memberAfterInsert: old) + } else { + (inserted: true, memberAfterInsert: newMember) + } + } + + public mutating func remove(_ member: Element) -> Element? { + return self.skipList.delete(member) + } + + public mutating func update(with newMember: __owned Element) -> Element? { + return if let old = self.skipList.setRepresentative(for: newMember) { + old + } else { + self.skipList.insert(newMember) + } + } + + public mutating func formUnion(_ other: __owned Self) { + self = self.union(other) + } + + public mutating func formIntersection(_ other: Self) { + self = self.intersection(other) + } + + public mutating func formSymmetricDifference(_ other: __owned Self) { + self = self.symmetricDifference(other) + } + + public func subtracting(_ other: Self) -> Self { + return .init( + sortedMerge( + between: self, + and: other, + retaining: .exclusivesToFirst, + sortingBy: TotalOrdering.areIncreasing + ) + ) + } + + public func isSubset(of other: Self) -> Bool { + return doesSortedMerger( + of: self, + and: other, + haveExclusivesToFirst: .mustBeAbsent, + haveExclusivesToSecond: .doNotCare, + haveSharedElements: .doNotCare, + sortingBy: TotalOrdering.areIncreasing + ) + } + + public func isDisjoint(with other: Self) -> Bool { + return doesSortedMerger( + of: self, + and: other, + haveExclusivesToFirst: .doNotCare, + haveExclusivesToSecond: .doNotCare, + haveSharedElements: .mustBeAbsent, + sortingBy: TotalOrdering.areIncreasing + ) + } + + public func isSuperset(of other: Self) -> Bool { + return doesSortedMerger( + of: self, + and: other, + haveExclusivesToFirst: .doNotCare, + haveExclusivesToSecond: .mustBeAbsent, + haveSharedElements: .doNotCare, + sortingBy: TotalOrdering.areIncreasing + ) + } + + public var isEmpty: Bool { self.count == 0 } + + // Use default implementation of `init(_:)`. + + public mutating func subtract(_ other: Self) { + self = self.subtracting(other) + } +} diff --git a/Sources/SortedCollections/SortedSetMerge.swift b/Sources/SortedCollections/SortedSetMerge.swift new file mode 100644 index 000000000..c49f58270 --- /dev/null +++ b/Sources/SortedCollections/SortedSetMerge.swift @@ -0,0 +1,552 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Collections open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +// +//===----------------------------------------------------------------------===// + +/// Utilities for merging two already-sorted sequences as if they were sets. + +// MARK: Raw Results of Sorted Set Mergers + +/// A comparison result emitted while merging two sorted sequences. +/// +/// Each result indicates whether the merged sequence element is sourced from +/// strictly to the first sequence, +/// strictly to the second sequence, +/// or present in both sequences (*i.e.*, shared). +/// +/// - SeeAlso: ``rawSortedMerge(of:and:sortingBy:)`` +public enum SortedSetMergeComparison { + /// Value occurs only in the first sequence. + case exclusiveToFirst(first: Element) + /// Value occurs only in the second sequence. + case exclusiveToSecond(second: Element) + /// Value occurs in both sequences; payloads are the equal values from each side. + case shared(first: Element, second: Element) +} + +/// A lazy sequence that walks two sorted input sequences and reports how their +/// elements relate to each other without coalescing duplicates. +/// +/// Use this when you need to know whether a value is unique to a side or +/// appears in both, e.g. to implement set-like operations. +public struct SortedSetRawMergingSequence +where First.Element == Second.Element { + let firstBase: First + let secondBase: Second + let areInIncreasingOrder: (First.Element, Second.Element) -> Bool +} + +extension SortedSetRawMergingSequence: LazySequenceProtocol { + public struct Iterator: IteratorProtocol { + var firstBase: First.Iterator + var secondBase: Second.Iterator + let areInIncreasingOrder: (First.Element, Second.Element) -> Bool + + fileprivate var firstCache: First.Element? + fileprivate var secondCache: Second.Element? + + public mutating func next() -> SortedSetMergeComparison? { + firstCache = firstCache ?? firstBase.next() + secondCache = secondCache ?? secondBase.next() + switch (firstCache, secondCache) { + case (let first?, let second?): + if areInIncreasingOrder(first, second) { + firstCache = nil + return .exclusiveToFirst(first: first) + } else if areInIncreasingOrder(second, first) { + secondCache = nil + return .exclusiveToSecond(second: second) + } else { + firstCache = nil + secondCache = nil + return .shared(first: first, second: second) + } + case (let first?, nil): + firstCache = nil + return .exclusiveToFirst(first: first) + case (nil, let second?): + secondCache = nil + return .exclusiveToSecond(second: second) + case (nil, nil): + return nil + } + } + } + + public func makeIterator() -> Iterator { + return .init( + firstBase: firstBase.makeIterator(), + secondBase: secondBase.makeIterator(), + areInIncreasingOrder: areInIncreasingOrder + ) + } + + public var underestimatedCount: Int { + switch ( + firstBase.underestimatedCount > 0, secondBase.underestimatedCount > 0 + ) { + case (false, false): + 0 + case (false, true): + secondBase.underestimatedCount + case (true, false): + firstBase.underestimatedCount + case (true, true): + Swift.min(firstBase.underestimatedCount, secondBase.underestimatedCount) + } + } +} + +/// Creates a lazy sequence that merges the two sequences sorted along +/// the given predicate to report the raw comparisons between them. +/// +/// - Parameters: +/// - first: The first input sequence, sorted by `areInIncreasingOrder`. +/// - second: The second input sequence, sorted by `areInIncreasingOrder`. +/// - areInIncreasingOrder: A strict weak ordering that defines ascending +/// order for elements. +/// - Returns: A lazy sequence of ``SortedSetMergeComparison`` values. +/// +/// - Note: Results are yielded in ascending order as determined by +/// `areInIncreasingOrder`. Equal elements across inputs are reported via a +/// single `.shared` result. +/// +/// - Note: Stability: For equal elements that originate from the same input +/// sequence, their original relative order is preserved when they are emitted +/// as exclusives. When a `.shared` is produced for equal elements across +/// inputs, it represents the pair in the order discovered by the merge. +/// +/// ### Example +/// ```swift +/// let a = [5, 4, 2, 1] +/// let b = [5, 3, 2] +/// let comparisons = rawSortedMerge(of: a, and: b, sortingBy: >) +/// for c in comparisons { +/// switch c { +/// case .exclusiveToFirst(let x): print("A-only: \(x)") +/// case .exclusiveToSecond(let y): print("B-only: \(y)") +/// case .shared(let x, _): print("Shared: \(x)") +/// } +/// } +/// // Prints (order may vary with iteration): +/// // Shared: 5 +/// // A-only: 4 +/// // B-only: 3 +/// // Shared: 2 +/// // A-only: 1 +/// ``` +public func rawSortedMerge( + of first: First, + and second: Second, + sortingBy areInIncreasingOrder: + @escaping (First.Element, Second.Element) -> Bool +) + -> SortedSetRawMergingSequence +where First: Sequence, Second: Sequence, First.Element == Second.Element { + return .init( + firstBase: first, + secondBase: second, + areInIncreasingOrder: areInIncreasingOrder + ) +} + +/// Creates a lazy sequence that merges the two sorted sequences to +/// report the raw comparisons between them. +/// +/// Convenience overload that uses `<` for `Comparable` elements. +/// +/// - Note: The emitted comparisons respect the ascending order of the inputs. +/// When elements compare equal, a single `.shared` is produced. +/// +/// ### Example +/// ```swift +/// let a = [1, 2, 4, 5] +/// let b = [2, 3, 5] +/// for c in rawSortedMerge(of: a, and: b) { +/// print(c) +/// } +/// // .exclusiveToFirst(1), .shared(2, 2), .exclusiveToSecond(3), .exclusiveToFirst(4), .shared(5, 5) +/// ``` +@inlinable +public func rawSortedMerge(of first: First, and second: Second) + -> SortedSetRawMergingSequence +where + First: Sequence, Second: Sequence, First.Element == Second.Element, + Second.Element: Comparable +{ + return rawSortedMerge(of: first, and: second, sortingBy: <) +} + +// MARK: - Sorted Set Merge + +/// A set of high-level operations that determine which elements to emit while +/// merging two sorted sequences. +/// +/// - SeeAlso: ``sortedMerge(between:and:retaining:sortingBy:)`` +public enum SetOperation: Int, CaseIterable { + /// Don't vend any element. + case nothing + /// Vend the elements that are from only the first sequence. + case exclusivesToFirst + /// Vend the elements that are from only the second sequence. + case exclusivesToSecond + /// Vend only the elements that appear in exactly one sequence. + case symmetricDifference + /// Vend only the elements that appear in both sequence, + /// using the first sequence's representative. + case intersection + /// Vend the elements from the first sequence. + case first + /// Vend the elements from the second sequence. + case second + /// Vend all elements, + /// collasping shared elements to the first sequence's representative. + case union + /// Vend all elements, + /// with both versions of a shared element being released. + case sum +} + +extension SetOperation { + /// Whether results should include elements that are only present in the first sequence. + @inlinable + public var includesExclusivesToFirst: Bool { rawValue & 0b1001 != 0 } + /// Whether results should include elements that are only present in the second sequence. + @inlinable + public var includesExclusivesToSecond: Bool { rawValue & 0b1010 != 0 } + /// Whether results should include elements that are present in both sequences. + @inlinable + public var includesSharedElements: Bool { rawValue & 0b1100 != 0 } +} + +/// A lazy sequence that emits elements from the two given inputs +/// sorted along the given predicate +/// according to the given set operation (*e.g.*, union, intersection). +/// +/// The merged output is also sorted along the given predicate. +/// Vended elements from the same source sequence retain their relative order in +/// the merged output. +/// +/// When `.sum` is the operation, +/// a shared element has both of its representative elements +/// vended consecutively, +/// with the one from the first source sequence vended first. +/// The other operations that vend shared elements only use one representative. +public struct SortedSetMergingSequence +where First.Element == Second.Element { + let firstBase: First + let secondBase: Second + let operation: SetOperation + let areInIncreasingOrder: (First.Element, Second.Element) -> Bool +} + +extension SortedSetMergingSequence: LazySequenceProtocol { + public struct Iterator: IteratorProtocol { + var rawIterator: SortedSetRawMergingSequence.Iterator + let permitExclusivesToFirst: Bool + let permitExclusivesToSecond: Bool + let maxSharedElementsAllowed: Int + + fileprivate var cache: Second.Element? + + public mutating func next() -> First.Element? { + if let secondOfShared = cache { + cache = nil + return secondOfShared + } + while let rawResult = rawIterator.next() { + switch rawResult { + case .exclusiveToFirst(let first) where permitExclusivesToFirst: + return first + case .exclusiveToSecond(let second) where permitExclusivesToSecond: + return second + case .shared(let first, let second) where maxSharedElementsAllowed > 0: + if maxSharedElementsAllowed > 1 { + cache = second + return first + } else { + return !permitExclusivesToFirst && permitExclusivesToSecond + ? second : first + } + case .exclusiveToFirst, .exclusiveToSecond, .shared: + continue + } + } + return nil + } + } + + public func makeIterator() -> Iterator { + return .init( + rawIterator: SortedSetRawMergingSequence( + firstBase: firstBase, + secondBase: secondBase, + areInIncreasingOrder: areInIncreasingOrder + ).makeIterator(), + permitExclusivesToFirst: operation.includesExclusivesToFirst, + permitExclusivesToSecond: operation.includesExclusivesToSecond, + maxSharedElementsAllowed: operation == .sum + ? 2 : operation == .union ? 1 : 0 + ) + } + + public var underestimatedCount: Int { + switch operation { + case .nothing: + 0 + case .exclusivesToFirst: + Swift.min( + firstBase.underestimatedCount - secondBase.underestimatedCount, + 0 + ) + case .exclusivesToSecond: + Swift.min( + secondBase.underestimatedCount - firstBase.underestimatedCount, + 0 + ) + case .symmetricDifference: + abs(firstBase.underestimatedCount - secondBase.underestimatedCount) + case .intersection: + 0 + case .first: + firstBase.underestimatedCount + case .second: + secondBase.underestimatedCount + case .union: + Swift.max(firstBase.underestimatedCount, secondBase.underestimatedCount) + case .sum: + firstBase.underestimatedCount + secondBase.underestimatedCount + } + } +} + +/// Creates a lazy sequence that performs a set-like merge of +/// the two given sequences sorted along the given predicate, +/// yielding the elements selected by the given desired result. +/// +/// - Parameters: +/// - first: The first sorted input sequence. +/// - second: The second sorted input sequence. +/// - subset: The set operation that controls which elements are emitted. +/// - areInIncreasingOrder: A strict ordering predicate shared by both inputs. +/// - Returns: A lazy sequence of elements from `first` and/or `second`. +/// +/// - SeeAlso: ``SetOperation`` +/// +/// ### Examples +/// ```swift +/// let a = [5, 4, 2, 1] +/// let b = [5, 3, 2] +/// +/// // Union: [5, 4, 3, 2, 1] +/// let unionSeq = sortedMerge(between: a, and: b, retaining: .union, sortingBy: >) +/// print(Array(unionSeq)) +/// +/// // Intersection: [5, 2] +/// let intersectionSeq = sortedMerge(between: a, and: b, retaining: .intersection, sortingBy: >) +/// print(Array(intersectionSeq)) +/// +/// // Symmetric difference: [4, 3, 1] +/// let symDiffSeq = sortedMerge(between: a, and: b, retaining: .symmetricDifference, sortingBy: >) +/// print(Array(symDiffSeq)) +/// +/// // Sum (multiset-style): shared elements appear twice -> [5, 5, 4, 3, 2, 2, 1] +/// let sumSeq = sortedMerge(between: a, and: b, retaining: .sum, sortingBy: >) +/// print(Array(sumSeq)) +/// ``` +public func sortedMerge( + between first: First, + and second: Second, + retaining subset: SetOperation, + sortingBy areInIncreasingOrder: + @escaping (First.Element, Second.Element) -> Bool +) + -> SortedSetMergingSequence +where First: Sequence, Second: Sequence, First.Element == Second.Element { + return .init( + firstBase: first, + secondBase: second, + operation: subset, + areInIncreasingOrder: areInIncreasingOrder + ) +} + +/// Creates a lazy sequence that performs a set-like merge of +/// the two given sorted sequences, +/// yielding the elements selected by the given desired result. +/// +/// Convenience overload that uses `<` for `Comparable` elements. +/// +/// - Note: The output is in ascending order. For shared elements, behavior is +/// controlled by `subset` +/// (e.g., `.union` once, `.sum` twice, `.intersection` once). +/// +/// ### Examples +/// ```swift +/// let a = [1, 2, 4, 5] +/// let b = [2, 3, 5] +/// print(Array(sortedMerge(between: a, and: b, retaining: .union))) // [1, 2, 3, 4, 5] +/// print(Array(sortedMerge(between: a, and: b, retaining: .intersection))) // [2, 5] +/// print(Array(sortedMerge(between: a, and: b, retaining: .symmetricDifference))) // [1, 3, 4] +/// print(Array(sortedMerge(between: a, and: b, retaining: .sum))) // [1, 2, 2, 3, 4, 5, 5] +/// ``` +@inlinable +public func sortedMerge( + between first: First, + and second: Second, + retaining subset: SetOperation +) + -> SortedSetMergingSequence +where + First: Sequence, Second: Sequence, First.Element == Second.Element, + Second.Element: Comparable +{ + return sortedMerge( + between: first, + and: second, + retaining: subset, + sortingBy: < + ) +} + +// MARK: - Sorted Set Merging Degree of Overlap + +/// Flags describing whether a given kind of overlap must or must not occur +/// between two sequences during a sorted merge. +/// +/// - SeeAlso: ``doesSortedMerger(of:and:haveExclusivesToFirst:haveExclusivesToSecond:haveSharedElements:sortingBy:)`` +public enum SetMergerOverlapFlags: Int, CaseIterable { + /// Merged sequences cannot have any element matching the category. + case mustBeAbsent = -1 + /// Ignore the category when checking results. + case doNotCare + /// Merged sequences must have at least one element matching the category. + case mustBePresent +} + +/// Checks whether a merge between the two given sequences +/// sorted along the given predicate will contain certain +/// kinds of overlap (exclusives or shared elements). +/// +/// This walks both inputs lazily until it can determine the answer. +/// +/// - Parameters: +/// - first: The first sorted input sequence. +/// - second: The second sorted input sequence. +/// - haveExclusivesToFirst: Requirement for elements that appear only in +/// `first`. +/// - haveExclusivesToSecond: Requirement for elements that appear only in +/// `second`. +/// - haveSharedElements: Requirement for elements present in both sequences. +/// - areInIncreasingOrder: The strict ordering predicate used by both inputs. +/// - Returns: `true` if the merge satisfies all requirements; +/// otherwise `false`. +/// +/// - SeeAlso: ``SetMergerOverlapFlags`` +/// +/// ### Example +/// ```swift +/// let a = [5, 4, 2, 1] +/// let b = [5, 3, 2] +/// let hasShared = doesSortedMerger( +/// of: a, +/// and: b, +/// haveExclusivesToFirst: .doNotCare, +/// haveExclusivesToSecond: .doNotCare, +/// haveSharedElements: .mustBePresent, +/// sortingBy: >) +/// // hasShared == true +/// ``` +public func doesSortedMerger( + of first: First, + and second: Second, + haveExclusivesToFirst: SetMergerOverlapFlags, + haveExclusivesToSecond: SetMergerOverlapFlags, + haveSharedElements: SetMergerOverlapFlags, + sortingBy areInIncreasingOrder: (First.Element, Second.Element) -> Bool +) -> Bool +where First: Sequence, Second: Sequence, First.Element == Second.Element { + return withoutActuallyEscaping(areInIncreasingOrder) { + var exclusiveToFirstCount = 0 + var exclusiveToSecondCount = 0 + var sharedCount = 0 + for find in SortedSetRawMergingSequence( + firstBase: first, + secondBase: second, + areInIncreasingOrder: $0 + ) { + switch find { + case .exclusiveToFirst(let first): + guard haveExclusivesToFirst != .mustBeAbsent else { return false } + + exclusiveToFirstCount += 1 + case .exclusiveToSecond(let second): + guard haveExclusivesToSecond != .mustBeAbsent else { return false } + + exclusiveToSecondCount += 1 + case .shared(let first, let second): + guard haveSharedElements != .mustBeAbsent else { return false } + + sharedCount += 1 + } + } + guard haveExclusivesToFirst != .mustBePresent || exclusiveToFirstCount > 0, + haveExclusivesToSecond != .mustBePresent || exclusiveToSecondCount > 0, + haveSharedElements != .mustBePresent || sharedCount > 0 + else { + return false + } + + return true + } +} + +/// Checks whether a merge between the two given sorted sequences will contain +/// certain kinds of overlap (exclusives or shared elements). +/// +/// Convenience overload that uses `<` for `Comparable` elements. +/// +/// - Note: Evaluation is lazy; the function stops as soon as it can decide the +/// answer based on the requirements. +/// +/// ### Example +/// ```swift +/// let a = [1, 2, 4, 5] +/// let b = [2, 3, 5] +/// let ok = doesSortedMerger( +/// of: a, +/// and: b, +/// haveExclusivesToFirst: .mustBePresent, +/// haveExclusivesToSecond: .mustBePresent, +/// haveSharedElements: .mustBePresent) +/// // ok == true +/// ``` +@inlinable +public func doesSortedMerger( + of first: First, + and second: Second, + haveExclusivesToFirst: SetMergerOverlapFlags, + haveExclusivesToSecond: SetMergerOverlapFlags, + haveSharedElements: SetMergerOverlapFlags +) -> Bool +where + First: Sequence, Second: Sequence, First.Element == Second.Element, + Second.Element: Comparable +{ + return doesSortedMerger( + of: first, + and: second, + haveExclusivesToFirst: haveExclusivesToFirst, + haveExclusivesToSecond: haveExclusivesToSecond, + haveSharedElements: haveSharedElements, + sortingBy: < + ) +}