From 506bf9035b6cb5ada4feb817398a62e75d2a795e Mon Sep 17 00:00:00 2001 From: Dzmitry Antonenka Date: Tue, 5 Sep 2023 00:56:25 +0300 Subject: [PATCH] feat(core): fold implemented --- .../ListTreeDataSource.swift | 27 +++++++++++- Tests/Shared/Helpers.swift | 4 +- .../MockData.swift | 43 ++++++++++++------ Tests/Shared/Model.swift | 44 +++++++++++++++++-- .../DebugDescriptionUtilsTests.swift | 13 +----- .../ListTreeDataSource+BasicTests.swift | 30 +++++++++++-- 6 files changed, 126 insertions(+), 35 deletions(-) rename Tests/{SwiftListTreeDataSourcePerformanceTests => Shared}/MockData.swift (81%) diff --git a/Sources/SwiftListTreeDataSource/ListTreeDataSource.swift b/Sources/SwiftListTreeDataSource/ListTreeDataSource.swift index 589f7d3..bfc1a9a 100644 --- a/Sources/SwiftListTreeDataSource/ListTreeDataSource.swift +++ b/Sources/SwiftListTreeDataSource/ListTreeDataSource.swift @@ -41,7 +41,19 @@ open class TreeItem: Hashable, Identifiable { } return counter } - + + public func fold(_ leaf: (Item) -> Result, cons: (Item, [Result]) -> Result) -> Result { + switch self.subitems { + case []: + return leaf(self.value) + case let nodes: + return cons( + self.value, + nodes.map { $0.fold(leaf, cons: cons) } + ) + } + } + public func allParents(of item: TreeItem) -> [TreeItem] { var parents: [TreeItem] = [] var currentItem: TreeItem = item @@ -89,7 +101,18 @@ open class ListTreeDataSource where ItemIdentifierType : Has func setShownFlatItems(_ items: [TreeItemType]) { self.shownFlatItems = items } - + + /// Folds created hierarchical store. + /// - Parameters: + /// - leaf: The leaf case + /// - cons: The cons case + /// - Returns: The folded hierarchical store. + public func fold(_ leaf: (ItemIdentifierType) -> Result, cons: (ItemIdentifierType, [Result]) -> Result) -> [Result] { + backingStore.map { node in + node.fold(leaf, cons: cons) + } + } + /// Adds the array of `items` to specified `parent`. /// - Parameters: /// - items: The array of items to add. diff --git a/Tests/Shared/Helpers.swift b/Tests/Shared/Helpers.swift index f2d993e..d3f82e2 100644 --- a/Tests/Shared/Helpers.swift +++ b/Tests/Shared/Helpers.swift @@ -4,7 +4,9 @@ import Foundation public func addItems(_ items: [OutlineItem], to snapshot: ListTreeDataSource) { addItems(items, itemChildren: { $0.subitems }, to: snapshot) } - +public func addItems(_ items: [NodeTestItem], to snapshot: ListTreeDataSource) { + addItems(items, itemChildren: { $0.subitems }, to: snapshot) +} public func depthFirstFlattened(items: [OutlineItem]) -> [OutlineItem] { return depthFirstFlattened(items: items, itemChildren: { $0.subitems }) } diff --git a/Tests/SwiftListTreeDataSourcePerformanceTests/MockData.swift b/Tests/Shared/MockData.swift similarity index 81% rename from Tests/SwiftListTreeDataSourcePerformanceTests/MockData.swift rename to Tests/Shared/MockData.swift index 931c543..e4921b6 100644 --- a/Tests/SwiftListTreeDataSourcePerformanceTests/MockData.swift +++ b/Tests/Shared/MockData.swift @@ -5,7 +5,6 @@ // Created by Dzmitry Antonenka on 18.04.21. // -import TestsShared import Foundation struct DataSet { @@ -25,7 +24,21 @@ struct DataSet { // Total elements in tree: (Int) $R0 = 7_174_452, creation time ~17.5 sec on MBP 2019, core i7 return menuItems(targetNestLevel: 14, currentLevel: 0, itemsInSection: 3) }() - + + static var mockDataTiny: [OutlineItem] = { + var items: [OutlineItem] = [ + OutlineItem(title: "Level 0, item1", subitems: [ + OutlineItem(title: "Level 1, item1"), + OutlineItem(title: "Level 1, item2", subitems: [ + OutlineItem(title: "Level 2, item1"), + OutlineItem(title: "Level 2, item2") + ]) + ]), + OutlineItem(title: "Level 0, item2") + ] + return items + }() + static var mockDataSmall: [OutlineItem] = { var items: [OutlineItem] = [ OutlineItem(title: "Compositional Layout", subitems: [ @@ -92,16 +105,17 @@ struct DataSet { } } - -enum MockData { +public enum MockData { + case tiny case small case large88K case large350K case doubleLarge797K case extraLarge7_2M - - var items: [OutlineItem] { + + public var items: [OutlineItem] { switch self { + case .tiny: return DataSet.mockDataTiny case .small: return DataSet.mockDataSmall case .large88K: return DataSet.mockData88K case .large350K: return DataSet.mockData350K @@ -111,15 +125,16 @@ enum MockData { } } -class DataManager { - static let shared = DataManager() +public class DataManager { + public static let shared = DataManager() - var mockData: MockData { self.mockDataSmall } + public var mockData: MockData { self.mockDataSmall } // specialized data sets - lazy var mockDataSmall: MockData = .small - lazy var mockDataLarge88K: MockData = .large88K - lazy var mockDataLarge350K: MockData = .large350K - lazy var mockDataDoubleLarge797K: MockData = .doubleLarge797K - lazy var mockDataExtraLarge7_2M: MockData = .extraLarge7_2M + public lazy var mockDataTiny: MockData = .tiny + public lazy var mockDataSmall: MockData = .small + public lazy var mockDataLarge88K: MockData = .large88K + public lazy var mockDataLarge350K: MockData = .large350K + public lazy var mockDataDoubleLarge797K: MockData = .doubleLarge797K + public lazy var mockDataExtraLarge7_2M: MockData = .extraLarge7_2M } diff --git a/Tests/Shared/Model.swift b/Tests/Shared/Model.swift index b5c2549..3abcf7d 100644 --- a/Tests/Shared/Model.swift +++ b/Tests/Shared/Model.swift @@ -1,21 +1,36 @@ import Foundation public class OutlineItem: Hashable { + public let identifier: UUID public let title: String public var subitems: [OutlineItem] - public init(title: String, - subitems: [OutlineItem] = []) { + public init( + identifier: UUID = UUID(), + title: String, + subitems: [OutlineItem] = [] + ) { + self.identifier = identifier self.title = title self.subitems = subitems } + + public init( + other: OutlineItem + ) { + self.identifier = other.identifier + self.title = other.title + self.subitems = other.subitems + } + public func hash(into hasher: inout Hasher) { + // don't add subitems recursively for performance reasons. hasher.combine(identifier) } public static func == (lhs: OutlineItem, rhs: OutlineItem) -> Bool { - return lhs.identifier == rhs.identifier + // don't add subitems recursively for performance reasons. + lhs.identifier == rhs.identifier } - private let identifier = UUID() } extension OutlineItem: CustomStringConvertible { @@ -24,3 +39,24 @@ extension OutlineItem: CustomStringConvertible { extension OutlineItem: CustomDebugStringConvertible { public var debugDescription: String { "\(title)" } } + +public struct NodeTestItem: Hashable { + public let identifier: UUID + public let title: String + public var subitems: [NodeTestItem] + public init(identifier: UUID, title: String, subitems: [NodeTestItem]) { + self.identifier = identifier + self.title = title + self.subitems = subitems + } +} +extension NodeTestItem { + public init(_ item: NodeTestItem) { + self.init(identifier: item.identifier, title: item.title, subitems: item.subitems) + } + public init(outline: OutlineItem) { + identifier = outline.identifier + title = outline.title + subitems = outline.subitems.map { NodeTestItem(outline: $0) } + } +} diff --git a/Tests/SwiftListTreeDataSourceTests/DebugDescriptionUtilsTests.swift b/Tests/SwiftListTreeDataSourceTests/DebugDescriptionUtilsTests.swift index eb21a87..97a6939 100644 --- a/Tests/SwiftListTreeDataSourceTests/DebugDescriptionUtilsTests.swift +++ b/Tests/SwiftListTreeDataSourceTests/DebugDescriptionUtilsTests.swift @@ -110,18 +110,9 @@ class DebugDescriptionUtilsTests: XCTestCase { // MARK: - Helpers func tinyHardcodedDataset() -> [OutlineItem] { - return [ - OutlineItem(title: "Level 0, item1", subitems: [ - OutlineItem(title: "Level 1, item1"), - OutlineItem(title: "Level 1, item2", subitems: [ - OutlineItem(title: "Level 2, item1"), - OutlineItem(title: "Level 2, item2") - ]) - ]), - OutlineItem(title: "Level 0, item2") - ] + DataManager.shared.mockDataTiny.items } - + func verifyExpandedLevelsDescriptionsMatchesTopLevelForFlattenedItems() { let backingStoreExpandedLevelsDescription = debugDescriptionExpandedLevels(sut.backingStore) let flattenedItemsTopLevelsDescription = debugDescriptionTopLevel(sut.items) diff --git a/Tests/SwiftListTreeDataSourceTests/ListTreeDataSource+BasicTests.swift b/Tests/SwiftListTreeDataSourceTests/ListTreeDataSource+BasicTests.swift index 23775da..8053904 100644 --- a/Tests/SwiftListTreeDataSourceTests/ListTreeDataSource+BasicTests.swift +++ b/Tests/SwiftListTreeDataSourceTests/ListTreeDataSource+BasicTests.swift @@ -24,8 +24,32 @@ class ListTreeDataSourceTests: XCTestCase { func setUpSut() { sut = ListTreeDataSource() } - - // MARK: - Append/Insert/Delete tests + + func test_foldRecreate_withTinyDataSet_shouldMatchIdentity() { + let sut = ListTreeDataSource() + + let dataSet = DataManager.shared.mockDataTiny.items.map(NodeTestItem.init(outline:)) + addItems(dataSet, to: sut) + sut.reload() + + let folded = sut.fold(NodeTestItem.init) { item, subitems in + NodeTestItem(identifier: item.identifier, title: item.title, subitems: subitems) + } + XCTAssertEqual(dataSet, folded) + } + + func test_foldId_withTinyDataSet_shouldMatchIdentity() { + let sut = ListTreeDataSource() + + let dataSet = DataManager.shared.mockDataTiny.items.map(NodeTestItem.init(outline:)) + addItems(dataSet, to: sut) + sut.reload() + + let folded = sut.fold(id(_:)) { root, _ in root } + XCTAssertEqual(dataSet, folded) + } + + // MARK: - Append/Insert/Delete/Move tests func test_append_withOneElementToNilParent_shouldAppendAsHead() throws { sut = ListTreeDataSource() // start from clean state @@ -459,4 +483,4 @@ class ListTreeDataSourceTests: XCTestCase { } } - +func id(_ a: A) -> A { a }