Signal-iOS/Signal/Calls/IdentifierIndexedArray.swift
2024-03-25 13:25:37 -05:00

120 lines
3.6 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalCoreKit
/// Represents a type that contains one or more identifiers, which may refer to
/// itself or any other type.
protocol ContainsIdentifiers<ContainedIdType> {
associatedtype ContainedIdType: Hashable
var containedIds: [ContainedIdType] { get }
}
/// An ordered list of elements that each contain one or more identifiers, which
/// can be indexed by the elements' contained identifiers.
struct IdentifierIndexedArray<Element: ContainsIdentifiers> {
/// All the elements managed by this indexed array.
private var elements: [Element]
/// Maps the contained IDs of each element to the index of that element
/// in ``elements``.
private var elementIndicesByIds: [Element.ContainedIdType: Int]
init(elements: [Element]) {
self.elements = elements
self.elementIndicesByIds = elements.indicesByEachContainedId()
}
// MARK: -
var allElements: [Element] { elements }
var count: Int { elements.count }
var isEmpty: Bool { elements.isEmpty }
var first: Element? { elements.first }
var last: Element? { elements.last }
subscript(index index: Int) -> Element {
return elements[index]
}
subscript(safe index: Int) -> Element? {
return elements[safe: index]
}
subscript(id id: Element.ContainedIdType) -> Element? {
guard let index = index(forId: id) else {
return nil
}
return self[index: index]
}
func index(forId id: Element.ContainedIdType) -> Int? {
return elementIndicesByIds[id]
}
/// Appends the given elements to this array.
/// - Note
/// Runs in `O(newElements.count)`.
mutating func append(newElements: [Element]) {
for element in newElements {
for id in element.containedIds {
elementIndicesByIds[id] = elements.count
}
elements.append(element)
}
}
/// Prepends the given elements to this array.
/// - Note
/// Runs in `O(self.count)`.
mutating func prepend(newElements: [Element]) {
elements = newElements + elements
elementIndicesByIds = elements.indicesByEachContainedId()
}
/// Replace the element at the given index with the given new element.
mutating func replace(
elementAtIndex indexToReplace: Int,
with newElement: Element
) {
let existingElement = elements[indexToReplace]
elements[indexToReplace] = newElement
for id in existingElement.containedIds {
elementIndicesByIds.removeValue(forKey: id)
}
for id in newElement.containedIds {
elementIndicesByIds[id] = indexToReplace
}
}
}
// MARK: -
private extension Array where Element: ContainsIdentifiers {
/// Computes a mapping of each ID contained within each element of this
/// array to that element's index in this array.
///
/// - Important
/// When an element of this array contains multiple IDs, each of those IDs
/// in the returned mapping will refer to the same index.
func indicesByEachContainedId() -> [Element.ContainedIdType: Int] {
return Dictionary(
self.enumerated().flatMap { (idx, element) -> [(Element.ContainedIdType, Int)] in
return element.containedIds.map { ($0, idx) }
},
uniquingKeysWith: { _, new in
owsFailDebug("Unexpectedly encountered duplicate IDs across ContainsIdentifiers elements!")
return new
}
)
}
}