Skip to content

Commit 88b9b33

Browse files
authored
Update BloomFilter (#261)
1 parent 7014a2e commit 88b9b33

File tree

2 files changed

+99
-14
lines changed

2 files changed

+99
-14
lines changed

Sources/OpenSwiftUICore/Data/Other/BloomFilter.swift

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,100 @@
11
//
22
// BloomFilter.swift
3-
// OpenSwiftUI
3+
// OpenSwiftUICore
44
//
55
// Audited for iOS 18.0
6-
// Status: WIP
6+
// Status: Complete
77

8+
/// A probabilistic data structure used to test whether an element is a member of a set.
9+
///
10+
/// A Bloom filter is a space-efficient probabilistic data structure that is used to test
11+
/// whether an element is a member of a set. False positives are possible, but false negatives
12+
/// are not. Elements can be added to the set, but not removed.
13+
///
14+
/// This implementation uses a simple bit array represented as a UInt and a hash function
15+
/// to set 3 different bits for each added element.
816
package struct BloomFilter: Equatable {
17+
/// The internal bit representation of the Bloom filter.
918
package var value: UInt
1019

20+
/// Creates an empty Bloom filter.
1121
@inlinable
1222
package init() {
1323
value = 0
1424
}
1525

26+
/// Creates a Bloom filter containing a single hashable value.
27+
///
28+
/// - Parameter value: The hashable value to add to the filter.
1629
@inlinable
1730
package init<T>(value: T) where T : Hashable {
1831
self.init(hashValue: value.hashValue)
1932
}
2033

34+
/// Creates a Bloom filter containing a specific type.
35+
///
36+
/// - Parameter type: The type to add to the filter.
2137
@inlinable
2238
package init(type: any Any.Type) {
2339
let pointer = unsafeBitCast(type, to: OpaquePointer.self)
2440
self.init(hashValue: Int(bitPattern: pointer))
2541
}
2642

43+
/// Adds the elements of another Bloom filter to this filter.
44+
///
45+
/// - Parameter other: Another Bloom filter to combine with this one.
2746
@inlinable
2847
package mutating func formUnion(_ other: BloomFilter) {
2948
value |= other.value
3049
}
3150

51+
/// Returns a new Bloom filter containing the elements of both this filter and another.
52+
///
53+
/// - Parameter other: Another Bloom filter to combine with this one.
54+
/// - Returns: A new Bloom filter containing the elements of both filters.
3255
@inlinable
3356
package func union(_ other: BloomFilter) -> BloomFilter {
34-
var filter = BloomFilter()
35-
filter.value = value | other.value
36-
return filter
57+
var value = self
58+
value.formUnion(other)
59+
return value
3760
}
3861

62+
/// Tests whether another Bloom filter might be contained in this one.
63+
///
64+
/// Due to the probabilistic nature of Bloom filters, this method may return true
65+
/// even when the other filter is not actually a subset (false positive). However,
66+
/// if it returns false, then the other filter definitely contains elements not in this one.
67+
///
68+
/// - Parameter other: Another Bloom filter to test against this one.
69+
/// - Returns: `true` if the other filter might be contained in this one, `false` if it definitely is not.
3970
@inlinable
4071
package func mayContain(_ other: BloomFilter) -> Bool {
4172
(other.value & ~value) == 0
4273
}
4374

75+
/// Indicates whether the Bloom filter is empty (contains no elements).
76+
///
77+
/// - Returns: `true` if the filter is empty, `false` otherwise.
4478
@inlinable
4579
package var isEmpty: Bool {
4680
value == 0
4781
}
4882

83+
/// Creates a Bloom filter using a hash value.
84+
///
85+
/// This initializer sets 3 different bits in the filter based on different parts of the hash value.
86+
///
87+
/// - Parameter hashValue: The hash value to use for bit selection.
4988
@inlinable
5089
init(hashValue: Int) {
5190
// Make sure we do LSR instead of ASR
5291
let value = UInt(bitPattern: hashValue)
53-
let a0 = 1 &<< (value &>> 0x10)
54-
let a1 = 1 &<< (value &>> 0xa)
55-
let a2 = 1 &<< (value &>> 0x4)
56-
self.value = a0 | a1 | a2
92+
let bit0 = 1 &<< (value &>> 0x10)
93+
let bit1 = 1 &<< (value &>> 0xa)
94+
let bit2 = 1 &<< (value &>> 0x4)
95+
self.value = bit0 | bit1 | bit2
5796
}
58-
97+
5998
// FIXME:
6099
@inline(__always)
61100
package func match(_ filter: BloomFilter) -> Bool {

Tests/OpenSwiftUICoreTests/Data/Other/BloomFilterTests.swift

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Testing
99
struct BloomFilterTests {
1010
#if os(macOS)
1111
@Test("Bloom Filter's init", .enabled(if: ProcessInfo.processInfo.operatingSystemVersionString == "14.0"))
12-
func testInitType() throws {
12+
func initType() throws {
1313
// hashValue: 0x1dd382138
1414
// 1 &<< (value &>> 0x10): 1 &<< 0x38 -> 0x0100_0000_0000_0000
1515
// 1 &<< (value &>> 0x0a): 1 &<< 0x08 -> 0x0000_0000_0000_0100
@@ -29,20 +29,66 @@ struct BloomFilterTests {
2929
#expect(BloomFilter(type: type).value == expectedValue)
3030
}
3131

32+
#if arch(x86_64) || arch(arm64)
3233
@Test
33-
func testInitHashValue() throws {
34+
func initHashValue() throws {
3435
// hashValue: 0
3536
// 1 &<< (value &>> 0x10): 1 &<< 0 -> 0x0000_0000_0000_0001
3637
// 1 &<< (value &>> 0x0a): 1 &<< 0 -> 0x0000_0000_0000_0001
3738
// 1 &<< (value &>> 0x04): 1 &<< 0 -> 0x0000_0000_0000_0001
3839
#expect(BloomFilter(hashValue: 0).value == 0x0000_0000_0000_0001)
3940

40-
#if arch(x86_64) || arch(arm64)
4141
// hashValue: 0x00000001dfa19ae0
4242
// 1 &<< (value &>> 0x10): 1 &<< 0x21 -> 0x0000_0002_0000_0000
4343
// 1 &<< (value &>> 0x0a): 1 &<< 0x26 -> 0x0000_0040_0000_0000
4444
// 1 &<< (value &>> 0x04): 1 &<< 0x2e -> 0x0000_4000_0000_0000
4545
#expect(BloomFilter(hashValue: 0x0000_0001_DFA1_9AE0).value == 0x0000_4042_0000_0000)
46-
#endif
4746
}
47+
48+
@Test
49+
func union() throws {
50+
// Test union of two filters
51+
let filter1 = BloomFilter(hashValue: 0) // 0x0000_0000_0000_0001
52+
let filter2 = BloomFilter(hashValue: 0x0000_0001_DFA1_9AE0) // 0x0000_4042_0000_0000
53+
54+
// Test union operation
55+
let unionResult = filter1.union(filter2)
56+
#expect(unionResult.value == 0x0000_4042_0000_0001)
57+
58+
// Test formUnion operation
59+
var mutableFilter = filter1
60+
mutableFilter.formUnion(filter2)
61+
#expect(mutableFilter.value == 0x0000_4042_0000_0001)
62+
63+
// Test union with empty filter
64+
let emptyFilter = BloomFilter()
65+
#expect(filter1.union(emptyFilter).value == filter1.value)
66+
#expect(emptyFilter.union(filter1).value == filter1.value)
67+
}
68+
69+
@Test
70+
func mayContain() throws {
71+
// Create filters with known bits
72+
let filter1 = BloomFilter(hashValue: 0) // 0x0000_0000_0000_0001
73+
let filter2 = BloomFilter(hashValue: 0x0000_0001_DFA1_9AE0) // 0x0000_4042_0000_0000
74+
let unionFilter = filter1.union(filter2) // 0x0000_4042_0000_0001
75+
let emptyFilter = BloomFilter() // 0x0000_0000_0000_0000
76+
77+
// Test mayContain functionality
78+
#expect(unionFilter.mayContain(filter1), "Union may contain filter1")
79+
#expect(unionFilter.mayContain(filter2), "Union may contain filter2")
80+
#expect(!filter1.mayContain(filter2), "filter1 does not contain filter2")
81+
#expect(!filter2.mayContain(filter1), "filter2 does not contain filter1")
82+
83+
// Empty filter cases
84+
#expect(filter1.mayContain(emptyFilter), "Any non-empty filter should contain empty filter")
85+
#expect(filter2.mayContain(emptyFilter), "Any non-empty filter should contain empty filter")
86+
#expect(!emptyFilter.mayContain(filter1), "Empty filter does not contain any non-empty filter")
87+
88+
// Self-containment
89+
#expect(filter1.mayContain(filter1), "Filter should contain itself")
90+
#expect(filter2.mayContain(filter2), "Filter should contain itself")
91+
#expect(emptyFilter.mayContain(emptyFilter), "Empty filter should contain itself")
92+
}
93+
#endif
4894
}

0 commit comments

Comments
 (0)