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
+ }
+}