From 00e009a82cf7e527161ec813b71990785e30bf41 Mon Sep 17 00:00:00 2001 From: Phil Viso Date: Sun, 8 Sep 2019 16:48:50 -0500 Subject: [PATCH] Added ability to group sorted articles by feed --- NetNewsWire.xcodeproj/project.pbxproj | 10 +- Shared/Data/ArticleUtilities.swift | 18 +++ Shared/Timeline/ArticleArray.swift | 17 +-- Shared/Timeline/ArticleSorter.swift | 57 +++++++++ .../NetNewsWireTests/ArticleArrayTests.swift | 109 ++++++++++++++++++ 5 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 Shared/Timeline/ArticleSorter.swift create mode 100644 Tests/NetNewsWireTests/ArticleArrayTests.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 49b9d9b98..cf30eedea 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -340,6 +340,8 @@ D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */; }; DD82AB0A231003F6002269DF /* SharingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82AB09231003F6002269DF /* SharingTests.swift */; }; DF999FF722B5AEFA0064B687 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF999FF622B5AEFA0064B687 /* SafariView.swift */; }; + FF3ABF13232599810074C542 /* ArticleArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF09232599450074C542 /* ArticleArrayTests.swift */; }; + FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -975,6 +977,8 @@ D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = ""; }; DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = ""; }; DF999FF622B5AEFA0064B687 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; + FF3ABF09232599450074C542 /* ArticleArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleArrayTests.swift; sourceTree = ""; }; + FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorter.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1254,8 +1258,9 @@ isa = PBXGroup; children = ( 84F204DF1FAACBB30076E152 /* ArticleArray.swift */, - 84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */, + FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */, 84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */, + 84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */, 849A97731ED9EC04007D329B /* TimelineStringFormatter.swift */, ); path = Timeline; @@ -1877,6 +1882,7 @@ isa = PBXGroup; children = ( 84F9EAD0213660A100CF2DE4 /* ScriptingTests */, + FF3ABF09232599450074C542 /* ArticleArrayTests.swift */, 84F9EAE3213660A100CF2DE4 /* NetNewsWireTests.swift */, DD82AB09231003F6002269DF /* SharingTests.swift */, 84F9EAE4213660A100CF2DE4 /* Info.plist */, @@ -2608,6 +2614,7 @@ 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, 8405DD8A2213E0E3008CE1BF /* DetailContainerView.swift in Sources */, 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */, + FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */, 84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */, 849A97791ED9EC04007D329B /* TimelineStringFormatter.swift in Sources */, 84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */, @@ -2686,6 +2693,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FF3ABF13232599810074C542 /* ArticleArrayTests.swift in Sources */, DD82AB0A231003F6002269DF /* SharingTests.swift in Sources */, 84F9EAEB213660A100CF2DE4 /* testIterativeCreateAndDeleteFeed.applescript in Sources */, 84F9EAF4213660A100CF2DE4 /* testGenericScript.applescript in Sources */, diff --git a/Shared/Data/ArticleUtilities.swift b/Shared/Data/ArticleUtilities.swift index 538f09bf3..bb46b045f 100644 --- a/Shared/Data/ArticleUtilities.swift +++ b/Shared/Data/ArticleUtilities.swift @@ -90,3 +90,21 @@ extension Article { } } + +// MARK: SortableArticle + +extension Article: SortableArticle { + + var sortableName: String { + return feed?.name ?? "" + } + + var sortableDate: Date { + return logicalDatePublished + } + + var sortableID: String { + return articleID + } + +} diff --git a/Shared/Timeline/ArticleArray.swift b/Shared/Timeline/ArticleArray.swift index 1e40e7593..60394f084 100644 --- a/Shared/Timeline/ArticleArray.swift +++ b/Shared/Timeline/ArticleArray.swift @@ -49,22 +49,11 @@ extension Array where Element == Article { return articleAtRow(oneIndex) }) } - - func sortedByDate(_ sortDirection: ComparisonResult) -> ArticleArray { - - let articles = sorted { (article1, article2) -> Bool in - if article1.logicalDatePublished == article2.logicalDatePublished { - return article1.articleID < article2.articleID - } - if sortDirection == .orderedDescending { - return article1.logicalDatePublished > article2.logicalDatePublished - } - return article1.logicalDatePublished < article2.logicalDatePublished - } - return articles + func sortedByDate(_ sortDirection: ComparisonResult, groupByFeed: Bool = false) -> ArticleArray { + return ArticleSorter.sortedByDate(articles: self, sortDirection: sortDirection, groupByFeed: groupByFeed) } - + func canMarkAllAsRead() -> Bool { return anyArticleIsUnread() diff --git a/Shared/Timeline/ArticleSorter.swift b/Shared/Timeline/ArticleSorter.swift new file mode 100644 index 000000000..d1b8814ce --- /dev/null +++ b/Shared/Timeline/ArticleSorter.swift @@ -0,0 +1,57 @@ +// +// ArticleSorter.swift +// NetNewsWire +// +// Created by Phil Viso on 9/8/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Articles +import Foundation + +protocol SortableArticle { + var sortableName: String { get } + var sortableDate: Date { get } + var sortableID: String { get } +} + +struct ArticleSorter { + + static func sortedByDate(articles: [T], + sortDirection: ComparisonResult, + groupByFeed: Bool) -> [T] { + let articles = articles.sorted { (article1, article2) -> Bool in + if groupByFeed { + let feedName1 = article1.sortableName + let feedName2 = article2.sortableName + + let comparison = feedName1.caseInsensitiveCompare(feedName2) + switch comparison { + case .orderedSame: + return isSortedByDate(article1, article2, sortDirection: sortDirection) + case .orderedAscending, .orderedDescending: + return comparison == .orderedAscending + } + } else { + return isSortedByDate(article1, article2, sortDirection: sortDirection) + } + } + + return articles + } + + // MARK: - + + private static func isSortedByDate(_ lhs: SortableArticle, + _ rhs: SortableArticle, + sortDirection: ComparisonResult) -> Bool { + if lhs.sortableDate == rhs.sortableDate { + return lhs.sortableID < rhs.sortableID + } + if sortDirection == .orderedDescending { + return lhs.sortableDate > rhs.sortableDate + } + return lhs.sortableDate < rhs.sortableDate + } + +} diff --git a/Tests/NetNewsWireTests/ArticleArrayTests.swift b/Tests/NetNewsWireTests/ArticleArrayTests.swift new file mode 100644 index 000000000..b5ed8d3d0 --- /dev/null +++ b/Tests/NetNewsWireTests/ArticleArrayTests.swift @@ -0,0 +1,109 @@ +// +// ArticleArrayTests.swift +// NetNewsWire +// +// Created by Phil Viso on 9/8/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import Articles +import Foundation +import XCTest + +@testable import NetNewsWire + +class ArticleArrayTests: XCTestCase { + + func testSortByDateAscending() { + let now = Date() + + // Test data includes a mixture of articles in the past and future, as well as articles with the same date + let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableID: "456") + let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableID: "789") + let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableID: "123") + let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableID: "345") + let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableID: "567") + let article6 = TestArticle(sortableName: "phil's Feed", sortableDate: Date(timeInterval: 60.0, since: now), sortableID: "567") + + let articles = [article1, article2, article3, article4, article5, article6] + let sortedArticles = ArticleSorter.sortedByDate(articles: articles, + sortDirection: .orderedAscending, + groupByFeed: false) + XCTAssertEqual(sortedArticles, [article5, article4, article3, article1, article2, article6]) + } + + func testSortByDateAscendingWithGroupByFeed() { + let now = Date() + + // Test data includes multiple groups (with case-insentive names), articles in the past and future, + // as well as articles with the same date + let article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -240.0, since: now), sortableID: "123") + let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableID: "456") + let article3 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableID: "234") + let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableID: "123") + let article5 = TestArticle(sortableName: "phil's feed", sortableDate: Date(timeInterval: 60.0, since: now), sortableID: "456") + let article6 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableID: "123") + let article7 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableID: "123") + let article8 = TestArticle(sortableName: "phil's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableID: "456") + let article9 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableID: "345") + let article10 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -15.0, since: now), sortableID: "123") + let article11 = TestArticle(sortableName: "Matt's Feed", sortableDate: Date(timeInterval: 60.0, since: now), sortableID: "123") + let article12 = TestArticle(sortableName: "Claire's Feed", sortableDate: now, sortableID: "123") + + let articles = [article1, article2, article3, article4, article5, article6, article7, article8, article9, article10, article11, article12] + let sortedArticles = ArticleSorter.sortedByDate(articles: articles, + sortDirection: .orderedAscending, + groupByFeed: true) + XCTAssertEqual(sortedArticles, [article12, article6, article3, article9, article11, article8, article2, article5, article1, article4, article7, article10]) + } + + func testSortByDateDescending() { + let now = Date() + + // Test data includes a mixture of articles in the past and future, as well as articles with the same date + let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableID: "456") + let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableID: "789") + let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableID: "123") + let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableID: "345") + let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableID: "567") + let article6 = TestArticle(sortableName: "phil's Feed", sortableDate: Date(timeInterval: 60.0, since: now), sortableID: "567") + + let articles = [article1, article2, article3, article4, article5, article6] + let sortedArticles = ArticleSorter.sortedByDate(articles: articles, + sortDirection: .orderedDescending, + groupByFeed: false) + XCTAssertEqual(sortedArticles, [article6, article3, article1, article2, article4, article5]) + } + + func testSortByDateDescendingWithGroupByFeed() { + let now = Date() + + // Test data includes multiple groups (with case-insentive names), articles in the past and future, + // as well as articles with the same date + let article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -240.0, since: now), sortableID: "123") + let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableID: "456") + let article3 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableID: "234") + let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableID: "123") + let article5 = TestArticle(sortableName: "phil's feed", sortableDate: Date(timeInterval: 60.0, since: now), sortableID: "456") + let article6 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableID: "123") + let article7 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableID: "123") + let article8 = TestArticle(sortableName: "phil's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableID: "456") + let article9 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableID: "345") + let article10 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -15.0, since: now), sortableID: "123") + let article11 = TestArticle(sortableName: "Matt's Feed", sortableDate: Date(timeInterval: 60.0, since: now), sortableID: "123") + let article12 = TestArticle(sortableName: "Claire's Feed", sortableDate: now, sortableID: "123") + + let articles = [article1, article2, article3, article4, article5, article6, article7, article8, article9, article10, article11, article12] + let sortedArticles = ArticleSorter.sortedByDate(articles: articles, + sortDirection: .orderedDescending, + groupByFeed: true) + XCTAssertEqual(sortedArticles, [article12, article11, article6, article3, article9, article5, article2, article8, article10, article7, article4, article1]) + } + +} + +private struct TestArticle: SortableArticle, Equatable { + let sortableName: String + let sortableDate: Date + let sortableID: String +}