Skip to content

Instantly share code, notes, and snippets.

@ivanopcode
Created November 10, 2024 15:52
Show Gist options
  • Save ivanopcode/8251fce762c6206157ef6f545062b219 to your computer and use it in GitHub Desktop.
Save ivanopcode/8251fce762c6206157ef6f545062b219 to your computer and use it in GitHub Desktop.
// MIT License
//
// Copyright (c) 2024 Ivan Oparin
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
/// # OrderedIDSet
///
/// `OrderedIDSet` представляет собой упорядоченное множество элементов, где уникальность элементов определяется их идентификатором `id`. Эта структура данных решает проблему, когда необходимо хранить коллекцию элементов с сохранением порядка вставки, обеспечивая при этом уникальность на основе бизнес-идентификатора, а не хэш-значения элемента.
///
/// В стандартной библиотеке Swift `Set` и `OrderedSet`(в пакете Swift Collections) обеспечивают уникальность элементов на основе их хэш-значения, что может быть нежелательно, если требуется уникальность по определенному свойству, например, идентификатору. `OrderedIDSet` использует `Identifiable` протокол для определения уникальности элементов по их `id`, обеспечивая быстрый доступ и модификацию элементов.
///
/// ## Основные характеристики:
/// - **Упорядоченность**: Элементы сохраняют порядок вставки.
/// - **Уникальность по `id`**: Элементы уникальны на основе их идентификатора.
/// - **Быстрый доступ**: Использует `OrderedDictionary` для обеспечения эффективного доступа к элементам.
///
/// ## Пример использования:
/// ```swift
/// struct User: Identifiable, Hashable {
/// let id: Int
/// let name: String
/// }
///
/// var users = OrderedIDSet<User>()
/// users.insert(User(id: 1, name: "Alice"))
/// users.insert(User(id: 2, name: "Bob"))
/// ```
///
public struct OrderedIDSet<Element: Identifiable & Hashable> {
/// Внутреннее хранилище элементов в виде упорядоченного словаря.
@usableFromInline
internal private(set) var _elements: OrderedDictionary<Element.ID, Element> = [:]
/// Количество элементов в множестве.
///
/// - Complexity: O(1)
public var count: Int {
_elements.count
}
/// Проверяет, является ли множество пустым.
///
/// - Complexity: O(1)
public var isEmpty: Bool {
_elements.isEmpty
}
/// Представление содержимого этой коллекции только для чтения в виде значения массива.
///
/// - Complexity: O(1)
public var elements: Array<Element> {
_elements.values.elements
}
/// Возвращает последний элемент упорядоченного множества.
///
/// Если упорядоченное множество пусто, возвращает nil.
///
/// - Complexity: O(1)
public var last: Element? {
_elements.values.last
}
/// Инициализатор с перечислением элементов.
///
/// - Parameter elements: Элементы для добавления в множество.
///
/// - Complexity: O(*n*), где *n* — количество элементов.
public init(elements: Element...) {
for element in elements {
_elements[element.id] = element
}
}
/// Инициализатор с массивом элементов.
///
/// - Parameter elements: Массив элементов для добавления в множество.
///
/// - Complexity: O(*n*), где *n* — количество элементов.
public init(elements: [Element]) {
for element in elements {
_elements[element.id] = element
}
}
/// Вставляет элемент в множество.
///
/// - Parameter element: Элемент для вставки.
/// - Returns: Кортеж, содержащий флаг успешной вставки и элемент после вставки.
///
/// - Complexity: Амортизированная O(1)
@discardableResult
public mutating func insert(_ element: Element) -> (inserted: Bool, memberAfterInsert: Element) {
if let existingElement = _elements[element.id] {
return (false, existingElement)
} else {
_elements[element.id] = element
return (true, element)
}
}
/// Обновляет элемент в множестве.
///
/// - Parameter newMember: Новый элемент для обновления.
/// - Returns: Старый элемент, если он существовал.
///
/// - Complexity: Амортизированная O(1)
// periphery:ignore
@discardableResult
public mutating func update(with newMember: Element) -> Element? {
let oldMember = _elements.updateValue(newMember, forKey: newMember.id)
return oldMember
}
/// Обновляет множество с помощью последовательности элементов.
///
/// - Parameter elements: Последовательность элементов для обновления.
///
/// - Complexity: O(*n*), где *n* — количество элементов в последовательности.
public mutating func update<S: Sequence>(with elements: S) where S.Element == Element {
for item in elements {
_elements.updateValue(item, forKey: item.id)
}
}
/// Удаляет элемент из множества.
///
/// - Parameter member: Элемент для удаления.
/// - Returns: Удаленный элемент, если он существовал.
///
/// - Complexity: Амортизированная O(1)
@discardableResult
public mutating func remove(_ member: Element) -> Element? {
return _elements.removeValue(forKey: member.id)
}
/// Удаляет элемент из множества.
///
/// - Parameter member: Элемент для удаления.
/// - Returns: Удаленный элемент, если он существовал.
///
/// - Complexity: Амортизированная O(1)
@discardableResult
public mutating func remove(byId memberId: Element.ID) -> Element? {
return _elements.removeValue(forKey: memberId)
}
/// Удаляет элементы из коллекции по предоставленному массиву идентификаторов.
///
/// - Parameter ids: Массив идентификаторов элементов, которые необходимо удалить.
///
/// - Complexity: O(k), где k - количество идентификаторов в массиве `ids`.
/// Для каждого идентификатора операция удаления выполняется за O(1).
public mutating func removeItems(withIDs ids: [Element.ID]) {
for id in ids {
_elements.removeValue(forKey: id)
}
}
/// Удаляет все элементы, удовлетворяющие заданному условию.
///
/// - Parameter shouldBeRemoved: Замыкание, определяющее, должен ли элемент быть удален.
///
/// - Complexity: O(*n*), где *n* — количество элементов в множестве.
public mutating func removeAll(where shouldBeRemoved: (Element) -> Bool) {
for key in _elements.keys {
if let element = _elements[key], shouldBeRemoved(element) {
_elements.removeValue(forKey: key)
}
}
}
/// Удаляет все элементы из множества.
///
/// После вызова этого метода множество становится пустым.
///
/// - Complexity: O(1)
public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) {
_elements.removeAll(keepingCapacity: keepCapacity)
}
/// Проверяет, содержит ли множество заданный элемент.
///
/// - Parameter element: Элемент для проверки.
/// - Returns: `true`, если элемент существует в множестве, иначе `false`.
///
/// - Complexity: O(1)
public func contains(_ element: Element) -> Bool {
_elements.keys.contains(element.id)
}
}
extension OrderedIDSet {
/// Сортирует элементы коллекции на месте в соответствии с предоставленным замыканием сравнения.
///
/// - Parameter areInIncreasingOrder: Замыкание, которое определяет порядок сортировки.
/// Возвращает `true`, если первый элемент должен располагаться перед вторым
/// в отсортированной последовательности.
///
/// - Complexity: O(n log n), где n - количество элементов в коллекции.
// periphery:ignore
mutating func sort(by areInIncreasingOrder: (Element, Element) -> Bool) {
let sortedElements = self.elements.sorted(by: areInIncreasingOrder)
self = OrderedIDSet(elements: sortedElements)
}
}
extension OrderedIDSet {
/// Инициализатор для создания пустого множества
///
/// Этот инициализатор эквивалентен инициализации с помощью пустого литерала массива.
///
/// - Complexity: O(1)
@inlinable
public init() {
_elements = [:]
}
}
extension OrderedIDSet {
// periphery:ignore
/// Группирует элементы по заданному ключу.
///
/// - Parameter keyForValue: Функция, возвращающая ключ для группировки каждого элемента.
/// - Returns: Словарь, где ключи — результаты функции `keyForValue`, а значения — массивы элементов.
///
/// - Complexity: O(*n*), где *n* — количество элементов в множестве.
public func grouping<Key: Hashable>(by keyForValue: (Element) -> Key) -> [Key: [Element]] {
var groupedDictionary: [Key: [Element]] = [:]
for element in self {
let key = keyForValue(element)
groupedDictionary[key, default: []].append(element)
}
return groupedDictionary
}
}
extension OrderedIDSet {
/// Предоставляет доступ к элементу коллекции по его идентификатору.
///
/// - Parameter id: Идентификатор искомого элемента.
/// - Returns: Элемент с указанным идентификатором, если он существует; в противном случае `nil`.
///
/// - Complexity: O(1)
// periphery:ignore
public subscript(id id: Element.ID) -> Element? {
return _elements[id]
}
}
internal extension OrderedIDSet {
mutating func updateElements(_ elements: OrderedDictionary<Element.ID, Element>) {
_elements = elements
}
mutating func updateElementById(_ elementId: Element.ID, element: Element) {
_elements[elementId] = element
}
}
extension OrderedIDSet: Equatable where Element: Equatable {}
extension OrderedIDSet: Hashable where Element: Hashable {}
//
// OrderedIDSet+Collection.swift
import OrderedCollections
extension OrderedIDSet: Collection {
/// Тип индекса для коллекции.
public typealias Index = OrderedDictionary<Element.ID, Element>.Index
/// Начальный индекс коллекции.
public var startIndex: Index {
_elements.elements.startIndex
}
/// Конечный индекс коллекции.
public var endIndex: Index {
_elements.elements.endIndex
}
/// Возвращает индекс после указанного.
///
/// - Parameter i: Текущий индекс.
/// - Returns: Следующий индекс в последовательности.
public func index(after i: Index) -> Index {
_elements.elements.index(after: i)
}
/// Возвращает элемент по индексу.
///
/// - Parameter position: Индекс элемента.
/// - Returns: Элемент по указанному индексу.
public subscript(position: Index) -> Element {
_elements.elements[position].value
}
}
extension OrderedIDSet: ExpressibleByArrayLiteral {
/// Инициализатор для создания множества из литерала массива.
///
/// - Parameter elements: Элементы для добавления.
///
/// - Complexity: O(*n*), где *n* — количество элементов.
public init(arrayLiteral elements: Element...) {
for element in elements {
updateElementById(element.id, element: element)
}
}
}
import OrderedCollections
/// Расширение OrderedIDSet для реализации протокола RandomAccessCollection.
/// RandomAccessCollection позволяет осуществлять произвольный доступ к элементам коллекции
/// с константным временем O(1).
extension OrderedIDSet: RandomAccessCollection {
/// Определяет тип индексов, используемый для доступа к элементам коллекции.
public typealias Indices = OrderedDictionary<Element.ID, Element>.Values.Indices
/// Возвращает индекс, непосредственно предшествующий указанному индексу.
///
/// - Parameter i: Действительный индекс коллекции.
/// - Returns: Индекс, находящийся непосредственно перед `i`.
///
/// - Complexity: O(1)
public func index(before i: Index) -> Index {
_elements.values.index(before: i)
}
/// Возвращает индекс, смещенный на указанное расстояние от заданного индекса.
///
/// - Parameters:
/// - i: Действительный индекс коллекции.
/// - distance: Величина смещения от индекса `i`.
/// - Returns: Индекс, смещенный на расстояние `distance` от индекса `i`.
///
/// - Complexity: O(1)
public func index(_ i: Index, offsetBy distance: Int) -> Index {
_elements.values.index(i, offsetBy: distance)
}
/// Возвращает расстояние между двумя индексами.
///
/// - Parameters:
/// - start: Начальный индекс.
/// - end: Конечный индекс.
/// - Returns: Количество элементов между индексами `start` и `end`.
///
/// - Complexity: O(1)
public func distance(from start: Index, to end: Index) -> Int {
_elements.values.distance(from: start, to: end)
}
}
//
// OrderedIDSet+RangeReplaceableCollection.swift
import OrderedCollections
extension OrderedIDSet: RangeReplaceableCollection {
/// Заменяет элементы в указанном диапазоне новыми элементами.
///
/// - Parameters:
/// - subrange: Диапазон для замены.
/// - newElements: Новые элементы для вставки.
///
/// - Complexity: O(*m*) в среднем, где *m* — общее количество элементов в диапазоне и новых элементах.
public mutating func replaceSubrange<C: Collection>(
_ subrange: Range<Index>,
with newElements: C
) where C.Element == Element {
var updatedDict = OrderedDictionary<Element.ID, Element>()
// Копируем элементы до subrange
for index in startIndex..<subrange.lowerBound {
let element = self[index]
updatedDict[element.id] = element
}
// Вставляем новые элементы
for element in newElements {
updatedDict[element.id] = element
}
// Копируем элементы после subrange
for index in subrange.upperBound..<endIndex {
let element = self[index]
updatedDict[element.id] = element
}
updateElements(updatedDict)
}
}
import Foundation
extension OrderedIDSet: Sequence {
/// Создает итератор для перебора элементов множества.
///
/// - Returns: Итератор по элементам множества.
///
/// - Complexity: O(1)
public func makeIterator() -> AnyIterator<Element> {
var iterator = _elements.values.makeIterator()
return AnyIterator {
return iterator.next()
}
}
}
// OrderedIDSetTests.swift
import XCTest
@testable import WBCommon
final class OrderedIDSetTests: XCTestCase {
struct TestElement: Identifiable, Hashable {
let id: Int
let value: String
}
func testEmptyInitialization() {
let emptySet = OrderedIDSet<TestElement>()
XCTAssertTrue(emptySet.isEmpty)
XCTAssertEqual(emptySet.count, 0)
}
func testInitializationWithElements() {
let element1 = TestElement(id: 1, value: "One")
let element2 = TestElement(id: 2, value: "Two")
let set = OrderedIDSet(elements: element1, element2)
XCTAssertEqual(set.count, 2)
XCTAssertEqual(set.elements, [element1, element2])
}
func testInsert() {
var set = OrderedIDSet<TestElement>()
let element1 = TestElement(id: 1, value: "One")
// вставка нового
let result1 = set.insert(element1)
XCTAssertTrue(result1.inserted)
XCTAssertEqual(set.count, 1)
// вставка дубликата
let result2 = set.insert(element1)
XCTAssertFalse(result2.inserted)
XCTAssertEqual(set.count, 1)
// вставка еще одного нового
let element2 = TestElement(id: 2, value: "Two")
let result3 = set.insert(element2)
XCTAssertTrue(result3.inserted)
XCTAssertEqual(set.count, 2)
}
func testUpdate() {
var set = OrderedIDSet<TestElement>()
let element1 = TestElement(id: 1, value: "One")
let element2 = TestElement(id: 1, value: "Uno") // дубликат по id
// обнолвние в пустом сете
let oldElement1 = set.update(with: element1)
XCTAssertNil(oldElement1)
XCTAssertEqual(set.count, 1)
// обновление существуюшего
let oldElement2 = set.update(with: element2)
XCTAssertEqual(oldElement2?.value, "One")
XCTAssertEqual(set.count, 1)
XCTAssertEqual(set[id: element1.id]?.value, "Uno")
}
func testUpdateWithSequence() {
var set = OrderedIDSet<TestElement>()
let elements = [
TestElement(id: 1, value: "One"),
TestElement(id: 2, value: "Two")
]
set.update(with: elements)
XCTAssertEqual(set.count, 2)
XCTAssertEqual(set.elements, elements)
let newElements = [
TestElement(id: 2, value: "Deux"),
TestElement(id: 3, value: "Three")
]
set.update(with: newElements)
XCTAssertEqual(set.count, 3)
XCTAssertEqual(set[id: 2]?.value, "Deux")
XCTAssertNotNil(set[id: 3])
}
func testRemove() {
var set = OrderedIDSet<TestElement>()
let element1 = TestElement(id: 1, value: "One")
let element2 = TestElement(id: 2, value: "Two")
set.insert(element1)
set.insert(element2)
// удаление существующего
let removedElement = set.remove(element1)
XCTAssertEqual(removedElement, element1)
XCTAssertEqual(set.count, 1)
XCTAssertFalse(set.contains(element1))
// удаление не существующего
let removedElementNil = set.remove(element1)
XCTAssertNil(removedElementNil)
XCTAssertEqual(set.count, 1)
}
func testRemoveByID() {
var set = OrderedIDSet<TestElement>()
let element = TestElement(id: 1, value: "One")
set.insert(element)
let removedElement = set.remove(byId: 1)
XCTAssertEqual(removedElement, element)
XCTAssertEqual(set.count, 0)
}
func testRemoveItemsWithIDs() {
var set = OrderedIDSet<TestElement>()
let elements = [
TestElement(id: 1, value: "One"),
TestElement(id: 2, value: "Two"),
TestElement(id: 3, value: "Three")
]
set.update(with: elements)
set.removeItems(withIDs: [1, 3])
XCTAssertEqual(set.count, 1)
XCTAssertNil(set[id: 1])
XCTAssertNotNil(set[id: 2])
XCTAssertNil(set[id: 3])
}
func testRemoveAllWhere() {
var set = OrderedIDSet<TestElement>()
let elements = [
TestElement(id: 1, value: "One"),
TestElement(id: 2, value: "Two"),
TestElement(id: 3, value: "Three")
]
set.update(with: elements)
set.removeAll { $0.id % 2 == 0 } // удалить четные
XCTAssertEqual(set.count, 2)
XCTAssertNotNil(set[id: 1])
XCTAssertNil(set[id: 2])
XCTAssertNotNil(set[id: 3])
}
func testContains() {
var set = OrderedIDSet<TestElement>()
let element = TestElement(id: 1, value: "One")
set.insert(element)
XCTAssertTrue(set.contains(element))
let otherElement = TestElement(id: 2, value: "Two")
XCTAssertFalse(set.contains(otherElement))
}
func testSubscriptByID() {
var set = OrderedIDSet<TestElement>()
let element = TestElement(id: 1, value: "One")
set.insert(element)
let retrievedElement = set[id: element.id]
XCTAssertEqual(retrievedElement, element)
let nonExistentElement = set[id: 2]
XCTAssertNil(nonExistentElement)
}
func testIteration() {
var set = OrderedIDSet<TestElement>()
let elements = [
TestElement(id: 1, value: "One"),
TestElement(id: 2, value: "Two"),
TestElement(id: 3, value: "Three")
]
set.update(with: elements)
var iteratedElements: [TestElement] = []
for element in set {
iteratedElements.append(element)
}
XCTAssertEqual(iteratedElements, elements)
}
func testSorting() {
var set = OrderedIDSet<TestElement>()
let elements = [
TestElement(id: 3, value: "Three"),
TestElement(id: 1, value: "One"),
TestElement(id: 2, value: "Two")
]
set.update(with: elements)
set.sort { $0.id < $1.id }
XCTAssertEqual(set.elements.map { $0.id }, [1, 2, 3])
}
func testRandomAccessCollectionConformance() {
var set = OrderedIDSet<TestElement>()
let elements = [
TestElement(id: 1, value: "One"),
TestElement(id: 2, value: "Two"),
TestElement(id: 3, value: "Three")
]
set.update(with: elements)
XCTAssertEqual(set.startIndex, 0)
XCTAssertEqual(set.endIndex, 3)
XCTAssertEqual(set[0], elements[0])
XCTAssertEqual(set[1], elements[1])
XCTAssertEqual(set[2], elements[2])
let index = set.index(set.startIndex, offsetBy: 2)
XCTAssertEqual(set[index], elements[2])
}
func testEquatableConformance() {
let element1 = TestElement(id: 1, value: "One")
let element2 = TestElement(id: 2, value: "Two")
var set1 = OrderedIDSet<TestElement>()
set1.insert(element1)
set1.insert(element2)
var set2 = OrderedIDSet<TestElement>()
set2.insert(element1)
set2.insert(element2)
XCTAssertEqual(set1, set2)
set2.remove(element2)
XCTAssertNotEqual(set1, set2)
}
func testHashableConformance() {
let element1 = TestElement(id: 1, value: "One")
let element2 = TestElement(id: 2, value: "Two")
var set1 = OrderedIDSet<TestElement>()
set1.insert(element1)
set1.insert(element2)
var set2 = OrderedIDSet<TestElement>()
set2.insert(element1)
set2.insert(element2)
let dict: [OrderedIDSet<TestElement>: String] = [set1: "First"]
XCTAssertEqual(dict[set2], "First")
}
func testExpressibleByArrayLiteral() {
let element1 = TestElement(id: 1, value: "One")
let element2 = TestElement(id: 2, value: "Two")
let set: OrderedIDSet<TestElement> = [element1, element2]
XCTAssertEqual(set.count, 2)
XCTAssertEqual(set.elements, [element1, element2])
}
func testGrouping() {
let elements = [
TestElement(id: 1, value: "Odd"),
TestElement(id: 2, value: "Even"),
TestElement(id: 3, value: "Odd"),
TestElement(id: 4, value: "Even")
]
let set = OrderedIDSet(elements: elements)
let grouped = set.grouping { $0.id % 2 == 0 ? "Even" : "Odd" }
XCTAssertEqual(grouped["Even"]?.count, 2)
XCTAssertEqual(grouped["Odd"]?.count, 2)
}
func testRemoveAll() {
var set = OrderedIDSet<TestElement>()
let elements = [
TestElement(id: 1, value: "One"),
TestElement(id: 2, value: "Two")
]
set.update(with: elements)
set.removeAll()
XCTAssertTrue(set.isEmpty)
XCTAssertEqual(set.count, 0)
}
func testLastProperty() {
var set = OrderedIDSet<TestElement>()
XCTAssertNil(set.last)
let element1 = TestElement(id: 1, value: "One")
let element2 = TestElement(id: 2, value: "Two")
set.insert(element1)
set.insert(element2)
XCTAssertEqual(set.last, element2)
}
func testIndexing() {
var set = OrderedIDSet<TestElement>()
let elements = [
TestElement(id: 1, value: "One"),
TestElement(id: 2, value: "Two"),
TestElement(id: 3, value: "Three")
]
set.update(with: elements)
let index = set.firstIndex { $0.id == 2 }
XCTAssertEqual(index, 1)
XCTAssertEqual(set[index!], elements[1])
}
func testReplaceSubrange() {
var set = OrderedIDSet<TestElement>()
let elements = [
TestElement(id: 1, value: "One"),
TestElement(id: 2, value: "Two"),
TestElement(id: 3, value: "Three")
]
set.update(with: elements)
let newElements = [
TestElement(id: 2, value: "Deux"),
TestElement(id: 4, value: "Four")
]
set.replaceSubrange(1...2, with: newElements)
XCTAssertEqual(set.count, 3)
XCTAssertEqual(set.elements.map { $0.id }, [1, 2, 4])
XCTAssertEqual(set[id: 2]?.value, "Deux")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment