From d3529bfdb41d94a0e6f6ba48e25e4dd4da13eadc Mon Sep 17 00:00:00 2001 From: fhasse95 <49185957+fhasse95@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:22:27 +0200 Subject: [PATCH] Added support for Dictionary(_:uniquingKeysWith:) --- README.md | 1 + Sources/SkipLib/Dictionary.swift | 1 - Sources/SkipLib/Skip/Dictionary.kt | 13 +++ Tests/SkipLibTests/DictionaryTests.swift | 109 +++++++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 003e514..3c1b9bf 100644 --- a/README.md +++ b/README.md @@ -370,6 +370,7 @@ Support levels:
  • init()
  • init(minimumCapacity: Int)
  • init(uniqueKeysWithValues keysAndValues: any Sequence<(Key, Value)>)
  • +
  • init(_ keysAndValues: any Sequence<(Key, Value)>, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows
  • func filter(_ isIncluded: ((Key, Value)) throws -> Bool) rethrows -> Dictionary<Key, Value>
  • subscript(key: Key) -> Value?
  • subscript(key: Key, default defaultValue: Value) -> Value
  • diff --git a/Sources/SkipLib/Dictionary.swift b/Sources/SkipLib/Dictionary.swift index f44865c..2d0f233 100644 --- a/Sources/SkipLib/Dictionary.swift +++ b/Sources/SkipLib/Dictionary.swift @@ -23,7 +23,6 @@ public struct Dictionary: Collection { public init(uniqueKeysWithValues keysAndValues: any Sequence<(Key, Value)>) { } - @available(*, unavailable) public init(_ keysAndValues: any Sequence<(Key, Value)>, uniquingKeysWith combine: (Value, Value) throws -> Value) rethrows { } diff --git a/Sources/SkipLib/Skip/Dictionary.kt b/Sources/SkipLib/Skip/Dictionary.kt index d0f57da..a5e1238 100644 --- a/Sources/SkipLib/Skip/Dictionary.kt +++ b/Sources/SkipLib/Skip/Dictionary.kt @@ -76,6 +76,19 @@ class Dictionary: Collection>, MutableStruct, KotlinConvertin _entryCollection = EntryCollection(this) } + @Suppress("UNCHECKED_CAST") + constructor(keysAndValues: Sequence>, uniquingKeysWith: (V, V) -> V): this() { + for (entry in keysAndValues.iterable) { + val key = entry.element0 + val value = entry.element1 + if (storage.containsKey(key)) { + storage[key] = uniquingKeysWith((storage[key] as V).sref(), value).sref() + } else { + storage[key] = value + } + } + } + @Suppress("UNCHECKED_CAST") constructor(map: Map, nocopy: Boolean = false, shared: Boolean = false) { if (nocopy && map is LinkedHashMap<*, *>) { diff --git a/Tests/SkipLibTests/DictionaryTests.swift b/Tests/SkipLibTests/DictionaryTests.swift index 5cb1265..bc15ab6 100644 --- a/Tests/SkipLibTests/DictionaryTests.swift +++ b/Tests/SkipLibTests/DictionaryTests.swift @@ -26,6 +26,108 @@ import Testing #expect(multipleElementDictionary["b"] == 2) } + @Test func uniquingKeysInitCombinesInSequenceOrder() { + let pairs = [("a", 1), ("b", 2), ("a", 3), ("a", 4)] + var combinedValues: [Int] = [] + + let result = Dictionary(pairs, uniquingKeysWith: { old, new in + combinedValues.append(old) + combinedValues.append(new) + return old * 10 + new + }) + + #expect(result == ["a": 134, "b": 2]) + #expect(combinedValues == [1, 3, 13, 4]) + } + + @Test func uniquingKeysInitCanKeepFirstOrLastValue() { + let pairs = [("a", 1), ("b", 2), ("a", 3)] + + let first = Dictionary(pairs, uniquingKeysWith: { old, _ in old }) + let last = Dictionary(pairs, uniquingKeysWith: { _, new in new }) + + #expect(first == ["a": 1, "b": 2]) + #expect(last == ["a": 3, "b": 2]) + } + + @Test func uniquingKeysInitPreservesNilValues() { + let pairs: [(String, Int?)] = [("a", nil), ("a", 1), ("a", nil), ("b", nil)] + var combinations = 0 + var sawInitialNil = false + + let result = Dictionary(pairs, uniquingKeysWith: { old, new in + if combinations == 0 { + sawInitialNil = old == nil + } + combinations += 1 + return new + }) + + #expect(combinations == 2) + #expect(sawInitialNil) + #expect(result.count == 2) + #expect(result.keys.contains("a")) + #expect(result.keys.contains("b")) + #expect(result.values.allSatisfy { $0 == nil }) + } + + @Test func uniquingKeysInitDoesNotCombineUniqueOrEmptyInput() { + let pairs = [("a", 1), ("b", 2)] + let emptyPairs: [(String, Int)] = [] + var combinations = 0 + let combine: (Int, Int) -> Int = { _, new in + combinations += 1 + return new + } + + let result = Dictionary(pairs, uniquingKeysWith: combine) + let empty = Dictionary(emptyPairs, uniquingKeysWith: combine) + + #expect(result == ["a": 1, "b": 2]) + #expect(empty.isEmpty) + #expect(combinations == 0) + } + + @Test func uniquingKeysInitPropagatesCombinerError() { + let pairs = [("a", 1), ("a", 2), ("a", 3)] + var combinations = 0 + var caughtExpectedError = false + let combine: (Int, Int) throws -> Int = { _, _ in + combinations += 1 + throw CombinationError.failed + } + + do { + _ = try Dictionary(pairs, uniquingKeysWith: combine) + } catch { + caughtExpectedError = error is CombinationError + } + + #expect(caughtExpectedError) + #expect(combinations == 1) + } + + @Test func uniquingKeysInitPreservesValueSemantics() { + var original = [1] + let pairs = [("a", original), ("a", [2])] + var combined: [Int] = [] + + var result = Dictionary(pairs, uniquingKeysWith: { old, new in + combined = old + new + return combined + }) + original.append(3) + combined.append(4) + var copy = result + copy["a"]?.append(5) + result["a"]?.append(6) + + #expect(pairs[0].1 == [1]) + #expect(result["a"] == [1, 2, 6]) + #expect(copy["a"] == [1, 2, 5]) + #expect(combined == [1, 2, 4]) + } + @Test func subscriptDidSet() { let holder = DictionaryHolder() holder.dictionary["a"] = 1 @@ -162,3 +264,10 @@ private class DictionaryHolder { } } } + +// MARK: - Test Errors +private extension DictionaryTests { + enum CombinationError: Error { + case failed + } +}