diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f11be4..f09462e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - name: Use Swift v6.0 uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: "16.0-beta" + xcode-version: "16.0" - name: Lint run: swift package plugin swiftlint --reporter github-actions-logging --strict - name: Build diff --git a/Package.resolved b/Package.resolved index ef1880d..23a96f4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "13e77cfe605311a25ca578c6b9723a9453537668a1626df5a3988b4fd2347ae7", + "originHash" : "2d2e97e160f96f8aa3f654aa41260ee9579635d1b832493b77c7ac0983927247", "pins" : [ + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" + } + }, { "identity" : "swiftlintplugins", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 74a7fb1..dbc70f4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,13 @@ // swift-tools-version: 5.10 import PackageDescription -var dependencies: [Package.Dependency] = [] +var dependencies: [Package.Dependency] = [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.3"), +] var plugins: [Target.PluginUsage]? #if os(macOS) -dependencies = [.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.56.2")] +dependencies += [.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.56.2")] plugins = [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")] #endif @@ -21,6 +23,9 @@ let package = Package( targets: [ .target( name: "BijectiveDictionary", + dependencies: [ + .product(name: "OrderedCollections", package: "swift-collections"), + ], swiftSettings: [.enableExperimentalFeature("StrictConcurrency")], plugins: plugins ), diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift index ee58ff2..e9ba1b2 100644 --- a/Package@swift-6.0.swift +++ b/Package@swift-6.0.swift @@ -1,11 +1,13 @@ // swift-tools-version: 6.0 import PackageDescription -var dependencies: [Package.Dependency] = [] +var dependencies: [Package.Dependency] = [ + .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.3"), +] var plugins: [Target.PluginUsage]? #if os(macOS) -dependencies = [.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.56.2")] +dependencies += [.package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.56.2")] plugins = [.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")] #endif @@ -21,6 +23,9 @@ let package = Package( targets: [ .target( name: "BijectiveDictionary", + dependencies: [ + .product(name: "OrderedCollections", package: "swift-collections"), + ], plugins: plugins ), .testTarget( @@ -29,5 +34,5 @@ let package = Package( plugins: plugins ), ], - swiftLanguageVersions: [.v6] + swiftLanguageModes: [.v6] ) diff --git a/Sources/BijectiveDictionary/Experiments/+_invariantCheck.swift b/Sources/BijectiveDictionary/Experiments/+_invariantCheck.swift new file mode 100644 index 0000000..4cabb9f --- /dev/null +++ b/Sources/BijectiveDictionary/Experiments/+_invariantCheck.swift @@ -0,0 +1,11 @@ +extension POCOrderedSetImplementation { +#if DEBUG + @usableFromInline @inline(never) + internal func _invariantCheck() { + print("👷🏼‍♀️ WIP: _invariantCheck is kept for consistency, but I'm not sure it's necessary, yet") + } +#else + @inlinable @inline(__always) + internal func _invariantCheck() {} +#endif +} diff --git a/Sources/BijectiveDictionary/Experiments/POC+Collection.swift b/Sources/BijectiveDictionary/Experiments/POC+Collection.swift new file mode 100644 index 0000000..539908a --- /dev/null +++ b/Sources/BijectiveDictionary/Experiments/POC+Collection.swift @@ -0,0 +1,37 @@ +// +// File.swift +// BijectiveDictionary +// +// Created by Daniel Lyons on 2024-09-16. +// + +import OrderedCollections + +extension POCOrderedSetImplementation: Collection { + public typealias Index = Int + public var startIndex: Int { 0 } + public var endIndex: Int { + assert(_ltr.count == _rtl.count) + return _ltr.count + } + + public func index(after index: Int) -> Int { + _ltr.index(after: index) + } + + public subscript(position: Int) -> (left: Left, right: Right) { + let leftValue = _ltr[position] + let rightValue = _rtl[position] + return (left: leftValue, right: rightValue) + } + + @inlinable public var isEmpty: Bool { + assert(_ltr.isEmpty == _rtl.isEmpty) + return _ltr.isEmpty + } + + @inlinable public var count: Int { + assert(_ltr.count == _rtl.count) + return _ltr.count + } +} diff --git a/Sources/BijectiveDictionary/Experiments/POC+ExpressibleByDictionaryLiteral.swift b/Sources/BijectiveDictionary/Experiments/POC+ExpressibleByDictionaryLiteral.swift new file mode 100644 index 0000000..4263a67 --- /dev/null +++ b/Sources/BijectiveDictionary/Experiments/POC+ExpressibleByDictionaryLiteral.swift @@ -0,0 +1,26 @@ +// +// File.swift +// BijectiveDictionary +// +// Created by Daniel Lyons on 2024-09-16. +// + +extension POCOrderedSetImplementation: ExpressibleByDictionaryLiteral { + /// Creates a new BijectiveDictionary from a dictionary literal. + /// + /// >Warning: Both left and right values must be unique or else this will fatal error. + public init(dictionaryLiteral elements: (Left, Right)...) { + self.init(minimumCapacity: elements.count) + + for (leftKey, rightKey) in elements { + let (leftInserted, atLeftIndex) = _ltr.append(leftKey) + let (rightInserted, atRightIndex) = _rtl.append(rightKey) + + guard leftInserted == true, + rightInserted == true, + atLeftIndex == atRightIndex else { + fatalError("dictionary literal contains duplicate value") + } + } + } +} diff --git a/Sources/BijectiveDictionary/Experiments/POC+Hashable.swift b/Sources/BijectiveDictionary/Experiments/POC+Hashable.swift new file mode 100644 index 0000000..b1cf835 --- /dev/null +++ b/Sources/BijectiveDictionary/Experiments/POC+Hashable.swift @@ -0,0 +1,10 @@ +extension POCOrderedSetImplementation: Hashable { + + public func hash(into hasher: inout Hasher) { + hasher.combine(_ltr) + } + + public static func == (lhs: POCOrderedSetImplementation, rhs: POCOrderedSetImplementation) -> Bool { + lhs._ltr == rhs._ltr + } +} diff --git a/Sources/BijectiveDictionary/Experiments/POC+Initializers.swift b/Sources/BijectiveDictionary/Experiments/POC+Initializers.swift new file mode 100644 index 0000000..29ee6fa --- /dev/null +++ b/Sources/BijectiveDictionary/Experiments/POC+Initializers.swift @@ -0,0 +1,64 @@ +// +// File.swift +// BijectiveDictionary +// +// Created by Daniel Lyons on 2024-09-16. +// + +import OrderedCollections + +extension POCOrderedSetImplementation { + @inlinable public init() { + self._ltr = OrderedSet() + self._rtl = OrderedSet() + } + + @inlinable public init(minimumCapacity: Int, persistent: Bool = false) { + self._ltr = OrderedSet(minimumCapacity: minimumCapacity, persistent: persistent) + self._rtl = OrderedSet(minimumCapacity: minimumCapacity, persistent: persistent) + } + + @inlinable public init(uniqueLeftRightPairs pairs: S) where S: Sequence, S.Element == (Left, Right) { + defer { _invariantCheck() } // necessary? + let leftValues = pairs.map(\.0) + self._ltr = OrderedSet(leftValues) + let rightValues = pairs.map(\.1) + self._rtl = OrderedSet(rightValues) + } + + /// Create an ordered `POCOrderedSetImplementation` from an unordered `Dictionary` + @inlinable public init?(_ dictionary: [Left: Right]) { + defer { _invariantCheck() } // necessary? + + let dictElements: [(Left, Right)] = dictionary.map { ($0.key, $0.value) } + self._ltr = OrderedSet(dictElements.map(\.0)) + self._rtl = OrderedSet(dictElements.map(\.1)) + + let leftValues = dictElements.map { $0 } + let rightValues = dictElements.map { $1 } + guard _ltr.count == leftValues.count, + _rtl.count == rightValues.count else { + // return `nil` if either set contains less elements + // i.e. if there were any duplicates + // ROOM FOR IMPROVEMENT: This approach doesn't check for duplicates until + // after the work has already been done. + return nil + } + } +} + +extension Dictionary { + @inlinable public init(_ poc: POCOrderedSetImplementation) where Value: Hashable { + self = poc.asDictionary + } +} + +extension POCOrderedSetImplementation { + @inlinable public var asElementsArray: [Element] { + return Array(zip(_ltr, _rtl)) + } + + @inlinable public var asDictionary: [Left: Right] { + Dictionary.init(uniqueKeysWithValues: asElementsArray) + } +} diff --git a/Sources/BijectiveDictionary/Experiments/POC+Operators.swift b/Sources/BijectiveDictionary/Experiments/POC+Operators.swift new file mode 100644 index 0000000..179501d --- /dev/null +++ b/Sources/BijectiveDictionary/Experiments/POC+Operators.swift @@ -0,0 +1,9 @@ +extension POCOrderedSetImplementation { + + static func == (lhs: Self, rhs: [Left: Right]) -> Bool { + lhs.asDictionary == rhs + } + static func == (lhs: [Left: Right], rhs: Self) -> Bool { + lhs == rhs.asDictionary + } +} diff --git a/Sources/BijectiveDictionary/Experiments/POC+Sendable.swift b/Sources/BijectiveDictionary/Experiments/POC+Sendable.swift new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Sources/BijectiveDictionary/Experiments/POC+Sendable.swift @@ -0,0 +1 @@ + diff --git a/Sources/BijectiveDictionary/Experiments/POC+Sequence.swift b/Sources/BijectiveDictionary/Experiments/POC+Sequence.swift new file mode 100644 index 0000000..d5b0562 --- /dev/null +++ b/Sources/BijectiveDictionary/Experiments/POC+Sequence.swift @@ -0,0 +1,49 @@ +// +// POCOrderedSetImplementation +// +// Created by Daniel Lyons on 2024-09-16. +// + +import OrderedCollections + +extension POCOrderedSetImplementation: Sequence { + public func makeIterator() -> Iterator { + Iterator( + _ltr: _ltr, + _rtl: _rtl, + start: 0, + end: _ltr.count - 1 + ) + } + + public typealias Element = (left: Left, right: Right) + + @frozen public struct Iterator: IteratorProtocol { + public init( + _ltr: OrderedSet, + _rtl: OrderedSet, + start: Int, + end: Int + ) { + self._ltr = _ltr + self._rtl = _rtl + self.current = start + self.end = end + } + + let _ltr: OrderedSet + let _rtl: OrderedSet + var current: Int + let end: Int + + public mutating func next() -> (left: Left, right: Right)? { + defer { current += 1 } + guard current < end else { + return nil + } + let leftValue = _ltr[current] + let rightValue = _rtl[current] + return (left: leftValue, right: rightValue) + } + } +} diff --git a/Sources/BijectiveDictionary/Experiments/POC_OrderedSetImplementation.swift b/Sources/BijectiveDictionary/Experiments/POC_OrderedSetImplementation.swift new file mode 100644 index 0000000..1871379 --- /dev/null +++ b/Sources/BijectiveDictionary/Experiments/POC_OrderedSetImplementation.swift @@ -0,0 +1,245 @@ +import OrderedCollections + +public struct POCOrderedSetImplementation { + /// An ordered set of left values + @usableFromInline internal var _ltr: OrderedSet + + /// An ordered set of right values + @usableFromInline internal var _rtl: OrderedSet + + func index(for leftValue: Left) -> Int? { + _ltr.firstIndex(of: leftValue) + } + + func index(for rightValue: Right) -> Int? { + _rtl.firstIndex(of: rightValue) + } + + /// A view of the capacity of the ordered view + /// + /// `POCOrderedSetImplementation` is backed by two `OrderedSet`, which does not have a capacity property. + /// However it does have an `elements` array. `capacityView` returns the `capactity` of the `elements` array. + @inlinable public var capacityView: Int { + return _ltr.elements.capacity + } + + @inlinable public mutating func reserveCapacity(_ minimumCapacity: Int) { + _ltr.reserveCapacity(minimumCapacity) + _rtl.reserveCapacity(minimumCapacity) + } + + @discardableResult + @inlinable public mutating func remove(byRight rightValue: Right) -> Left? { + defer { _invariantCheck() } // unnecessary? + guard let rightIndex = _rtl.firstIndex(of: rightValue) else { + return nil + } + _rtl.remove(at: rightIndex) + let leftValue = _ltr[rightIndex] + _ltr.remove(at: rightIndex) + return leftValue + } + + @discardableResult + @inlinable public mutating func remove(byLeft leftValue: Left) -> Right? { + defer { _invariantCheck() } // unnecessary? + guard let leftIndex = _ltr.firstIndex(of: leftValue) else { + return nil + } + _ltr.remove(at: leftIndex) + let rightValue = _rtl[leftIndex] + _rtl.remove(at: leftIndex) + return rightValue + } + + @inlinable public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) { + _ltr.removeAll(keepingCapacity: keepCapacity) + _rtl.removeAll(keepingCapacity: keepCapacity) + } + + @inlinable public subscript( + left leftValue: Left, + default defaultValue: @autoclosure () -> Right + ) -> Right { + get { self[left: leftValue] ?? defaultValue() } + set(right) { self[left: leftValue] = right } + } + + @inlinable public subscript( + right rightValue: Right, + default defaultValue: @autoclosure () -> Left + ) -> Left { + get { self[right: rightValue] ?? defaultValue() } + set(left) { self[right: rightValue] = left } + } + + @inlinable public subscript(left leftValue: Left) -> Right? { + get { + guard let leftIndex = _ltr.firstIndex(of: leftValue) else { + return nil + } + return _rtl[safe: leftIndex] + } + set(newRightValue) { + defer { _invariantCheck() } // unnecessary? + guard let newRightValue else { + // Right value being set to `nil`. + + // I'm not sure what to do here. + // I don't think Left or Right is guaranteed to be an Optional here + guard let index = _ltr.firstIndex(of: leftValue) else { + // setting right value to `nil`, by a non-existent left value + return // do nothing + } + // nil right value, real left value + _ltr.remove(at: index) // remove both + _rtl.remove(at: index) + return + } + guard let index = _ltr.firstIndex(of: leftValue) else { + // Inserting new left-right pair. + let (leftInserted, atLeftIndex) = _ltr.append(leftValue) + let (rightInserted, atRightIndex) = _rtl.append(newRightValue) + + guard leftInserted == true, rightInserted == true, + atLeftIndex == atRightIndex else { + fatalError(""" +error occured while inserting right value: \(newRightValue) by left: \(leftValue) through subscript +""") + } + return + } + + // Updating left-right pair (by left)... +// // We've already guaranteed that the left value is present +// // so it will update (not append) +// _ltr.updateOrAppend(leftValue) + _rtl.elements[index] = newRightValue // O(n) + } + } + + @inlinable public subscript(right rightValue: Right) -> Left? { + get { + guard let rightIndex = _rtl.firstIndex(of: rightValue) else { + return nil + } + return _ltr[safe: rightIndex] + } + set(newLeftValue) { + defer { _invariantCheck() } + guard let newLeftValue else { + // Left value being set to `nil`. + + // I'm not sure what to do here. + // I don't think Left or Right is guaranteed to be an Optional here + guard let index = _rtl.firstIndex(of: rightValue) else { + // setting left value to `nil`, by a non-existent right value + return // do nothing + } + // `nil` left value, real right value + _ltr.remove(at: index) + _rtl.remove(at: index) + return + } + guard let index = _rtl.firstIndex(of: rightValue) else { + // Inserting new left-right pair. + let (leftInserted, atLeftIndex) = _ltr.append(newLeftValue) + let (rightInserted, atRightIndex) = _rtl.append(rightValue) + + guard leftInserted == true, rightInserted == true, + atLeftIndex == atRightIndex else { + fatalError(""" +error occured while inserting left value: \(newLeftValue) by right: \(rightValue) through subscript +""") + } + return + } + + // Updating left-right pair (by right)... + _ltr.elements[index] = newLeftValue +// // We've already guaranteed that the right value is present +// // so it will update (not append) +// _rtl.updateOrAppend(rightValue) + + } + } + + @inlinable public func indexByLeft(_ leftValue: Left) -> Int? { + _ltr.firstIndex(of: leftValue) + } + + @inlinable public func indexByRight(_ rightValue: Right) -> Int? { + _rtl.firstIndex(of: rightValue) + } + + @inlinable public func findPairByLeft(_ leftValue: Left) -> Element? { + guard let rightValue = self[left: leftValue] else { return nil } + return (left: leftValue, right: rightValue) + } + + @inlinable public func findPairByRight(_ rightValue: Right) -> Element? { + guard let leftValue = self[right: rightValue] else { return nil } + return (left: leftValue, right: rightValue) + } +} + +// MARK: Sendable +extension POCOrderedSetImplementation: Sendable +where Left: Sendable, Right: Sendable {} + +// MARK: Codable +extension POCOrderedSetImplementation: Encodable where Left: Encodable, Right: Encodable { + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(self.asDictionary) + } +} + +extension POCOrderedSetImplementation: Decodable where Left: Decodable, Right: Decodable { + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let rawKeyedDictionary = try container.decode([Left: Right].self) + + guard let dict = Self(rawKeyedDictionary) else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "The decoded dictionary must have unique keys and unique values." + ) + } + self = dict + } +} + +extension OrderedSet { + @inlinable + public subscript(safe index: Index) -> Element? { + guard index >= 0 && index < count else { + return nil + } + return self[index] + } + + /// Replaces the old value (if it is present) with the new value + /// + /// Will do nothing if the old value is not present. + /// Replaced + @available(*, deprecated, message: "WIP: Unfinished") + @discardableResult @inlinable + public func replace(old oldValue: Element, withNew newValue: Element) -> (replaced: Bool, index: Int) { + guard oldValue != newValue else { + // New and old value are the same so let's do nothing. + let index = self.firstIndex(of: oldValue) + return (replaced: false, index: index ?? -1) + } + + if self.contains(oldValue) { + // remove old value + // append new value + return (replaced: true, index: -1) + } else { + return (replaced: false, index: -1) + + } + + } +} diff --git a/Tests/BijectiveDictionaryTests/BijectiveDictionaryTests.swift b/Tests/BijectiveDictionaryTests/BijectiveDictionaryTests.swift index f856dbf..8039c8f 100644 --- a/Tests/BijectiveDictionaryTests/BijectiveDictionaryTests.swift +++ b/Tests/BijectiveDictionaryTests/BijectiveDictionaryTests.swift @@ -21,8 +21,9 @@ import Testing #expect(fromInit.count == 0) } -@Test func createWithCapacity() { - let dict = BijectiveDictionary(minimumCapacity: 10) +@Test(arguments: [10, 100, 1000, 10_000, 100_000, 1_000_000]) +func createWithCapacity(minimumCapacity: Int) { + let dict = BijectiveDictionary(minimumCapacity: minimumCapacity) #expect(dict.capacity >= 10) } @@ -39,6 +40,7 @@ import Testing func fromStandardDictionary(standardDict: [String: Int]) throws { let dict = try #require(BijectiveDictionary(standardDict)) #expect(dict.count == standardDict.count) + #expect(dict == standardDict) // I'm not sure why this works. } @Test func fromStandardDictionaryInvalid() { diff --git a/Tests/BijectiveDictionaryTests/POCTests.swift b/Tests/BijectiveDictionaryTests/POCTests.swift new file mode 100644 index 0000000..bded267 --- /dev/null +++ b/Tests/BijectiveDictionaryTests/POCTests.swift @@ -0,0 +1,243 @@ +// ============================================================= +// File: POCTests.swift +// Project: BijectiveDictionary +// ------------------------------------------------------------- +// Created by Daniel Lyons on 09/16/2024 +// Copyright © 2024 Daniel Lyons. All rights reserved. +// ============================================================= + +#if swift(>=6.0) +import Foundation +import Testing +@testable import BijectiveDictionary + +@Test func _createEmpty() { +// let fromLiteral: POCOrderedSetImplementation = [:] +// #expect(fromLiteral.isEmpty) +// #expect(fromLiteral.count == 0) + + let fromInit = POCOrderedSetImplementation() + #expect(fromInit.isEmpty) + #expect(fromInit.count == 0) +} + +@Test(arguments: [10, 100, 1000, 10_000, 100_000, 1_000_000]) +func _createWithCapacity(minimumCapacity: Int) { + let dict = POCOrderedSetImplementation(minimumCapacity: minimumCapacity) + + #expect(dict.capacityView >= 10) +} + +@Test func _fromUniqueLeftRightPairs() { + let uniquePairs = [ + (left: "A", right: 1), + (left: "B", right: 2), + (left: "C", right: 3) + ] + let dict = POCOrderedSetImplementation(uniqueLeftRightPairs: uniquePairs) + #expect(dict.count == 3) +} + +@Test(arguments: [[:], ["A": 1, "B": 2, "C": 3]]) +func _fromStandardDictionary(standardDict: [String: Int]) throws { + let dict = try #require(POCOrderedSetImplementation(standardDict)) + #expect(dict.count == standardDict.count) +} + +@Test func _fromStandardDictionaryInvalid() { + let nonUniqueRightValues = ["A": 1, "B": 2, "C": 1] + #expect(POCOrderedSetImplementation(nonUniqueRightValues) == nil) +} + +@Test(arguments: [[:], ["A": 1, "B": 2, "C": 3]]) +func _toStandardDictionary(dict: POCOrderedSetImplementation) { + let standardDict = Dictionary(dict) + #expect(standardDict.count == dict.count) +} + +@Test func _subscriptGetByLeft() { + let dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + + #expect(dict[left: "A"] == 1) + #expect(dict[left: "B"] == 2) + #expect(dict[left: "C"] == 3) + #expect(dict[left: "D"] == nil) +} + +@Test func _subscriptGetByRight() { + let dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + + #expect(dict[right: 1] == "A") + #expect(dict[right: 2] == "B") + #expect(dict[right: 3] == "C") + #expect(dict[right: 4] == nil) +} + +@Test func _subscriptSetByLeft() { + var dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + + dict[left: "A"] = 4 + #expect(dict[left: "A"] == 4, "Value should persist after set operation") + #expect(dict[right: 4] == "A", "Reverse mapping should hold") + #expect(dict[right: 1] == nil, "Previous mapping should no longer hold") + + dict[left: "A"] = 5 + #expect(dict[right: 1] == nil, "Previous mapping should no longer hold") + + dict[left: "A"] = nil + #expect(dict[left: "A"] == nil) + + dict[left: "D"] = nil + #expect(dict[left: "D"] == nil) +} + +@Test func _subscriptSetByRight() { + var dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + + dict[right: 3] = "D" + #expect(dict[right: 3] == "D", "Value should persist after set operation") + #expect(dict[left: "D"] == 3, "Reverse mapping should hold") + #expect(dict[left: "C"] == nil, "Previous mapping should no longer hold") + + dict[right: 3] = "E" + #expect(dict[right: 3] == "E") + + dict[right: 3] = nil + #expect(dict[right: 3] == nil) + + dict[right: 4] = nil + #expect(dict[right: 4] == nil) +} + +@Test func _subscriptGettersWithDefault() { + let dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + + #expect(dict[left: "D", default: 4] == 4, "Should return default") + #expect(dict[left: "A", default: -1] == 1, "Should not return default") + + #expect(dict[right: 4, default: "D"] == "D", "Should return default") + #expect(dict[right: 1, default: "Z"] == "A", "Should not return default") +} + +@Test func _subscriptSettersWithDefault() { + var dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + + dict[left: "D", default: 4] += 1 + #expect(dict[left: "D"] == 5, "Should use default value") + + dict[left: "A", default: 4] += 1 + #expect(dict[left: "A"] == 2, "Should not use default value") +} + +@Test(arguments: [[:], ["A": 1, "B": 2, "C": 3]]) +func _hashable(dict: POCOrderedSetImplementation) { + let copy = dict + #expect(dict.hashValue == copy.hashValue) + #expect(dict == copy) + + let otherDict: POCOrderedSetImplementation = ["X": 2, "Y": 3, "Z": 4] + #expect(dict.hashValue != otherDict.hashValue) + #expect(dict != otherDict) +} + +@Test func _equalWithStandardDictionary() { + let standardDict = ["A": 1, "B": 2, "C": 3] + #expect(POCOrderedSetImplementation(standardDict)! == standardDict) + #expect(standardDict == POCOrderedSetImplementation(standardDict)!) +} + +@Test(arguments: [[:], ["A": 1, "B": 2, "C": 3]]) +func _removeAll(dict: POCOrderedSetImplementation) { + var dict = dict + dict.removeAll() + #expect(dict.isEmpty) +} + +@Test func _removeByRight() { + var dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + + #expect(dict.remove(byRight: 3) == "C") + #expect(dict[left: "C"] == nil) + #expect(dict[right: 3] == nil) + + #expect(dict.remove(byRight: 4) == nil) +} + +@Test func _removeByLeft() { + var dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + + #expect(dict.remove(byLeft: "C") == 3) + #expect(dict[left: "C"] == nil) + #expect(dict[right: 3] == nil) + + #expect(dict.remove(byLeft: "D") == nil) +} + +@Test(arguments: [[:], ["A": 1, "B": 2, "C": 3]]) +func _sequence(dict: POCOrderedSetImplementation) { + + for (leftValue, rightValue) in dict { + #expect(dict[left: leftValue] == rightValue) + #expect(dict[right: rightValue] == leftValue) + } +} + +@Test(arguments: [[:], ["A": 1, "B": 2, "C": 3]]) +func _collection(dict: POCOrderedSetImplementation) { + + #expect(dict.startIndex <= dict.endIndex) + + for index in dict.indices { + let (leftValue, rightValue) = dict[index] + #expect(dict[left: leftValue] == rightValue) + } +} + +@Test("Encodable behavior should be equivalent to `Dictionary`") +func _encodableConformance() throws { + let dict = ["A": 1, "B": 2, "C": 3] + let bijectiveDict = POCOrderedSetImplementation(dict) + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + + let dictData = try encoder.encode(dict) + let dictJSONString = String(decoding: dictData, as: UTF8.self) + + let bijectiveDictData = try encoder.encode(bijectiveDict) + let bijectiveDictJSONString = String(decoding: bijectiveDictData, as: UTF8.self) + + #expect(dictData == bijectiveDictData) + #expect(dictJSONString == bijectiveDictJSONString) +} + +@Test("Decodable behavior should be equivalent to `Dictionary`") +func _decodableConformance() throws { + let jsonData = Data(#"{ "A": 1, "B": 2, "C": 3 }"#.utf8) + + let decoder = JSONDecoder() + let decoded = try decoder.decode(POCOrderedSetImplementation.self, from: jsonData) + + let control = POCOrderedSetImplementation(["A": 1, "B": 2, "C": 3]) + withKnownIssue( + "Order is non-predictable once the data is decoded and turned into a Dictionary", + isIntermittent: true) { + #expect(decoded == control) + } +} + +@Test +func _findPairByLeft() { + let dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + #expect(dict.findPairByLeft("A")?.left == "A") + #expect(dict.findPairByLeft("A")?.right == 1) + #expect(dict.findPairByLeft("D") == nil) +} + +@Test +func _findPairByRight() { + let dict: POCOrderedSetImplementation = ["A": 1, "B": 2, "C": 3] + #expect(dict.findPairByRight(1)?.left == "A") + #expect(dict.findPairByRight(1)?.right == 1) + #expect(dict.findPairByRight(4) == nil) +} +#endif