Re-worked sorting logic to handle multiple feeds having the same name

This commit is contained in:
Phil Viso 2019-09-13 07:43:28 -05:00
parent cf404859e4
commit 269364a337
3 changed files with 234 additions and 87 deletions

View File

@ -103,8 +103,12 @@ extension Article: SortableArticle {
return logicalDatePublished
}
var sortableID: String {
var sortableArticleID: String {
return articleID
}
var sortableFeedID: String {
return feedID
}
}

View File

@ -12,46 +12,58 @@ import Foundation
protocol SortableArticle {
var sortableName: String { get }
var sortableDate: Date { get }
var sortableID: String { get }
var sortableArticleID: String { get }
var sortableFeedID: String { get }
}
struct ArticleSorter {
static func sortedByDate<T: SortableArticle>(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)
}
if groupByFeed {
return sortedByFeedName(articles: articles, sortByDateDirection: sortDirection)
} else {
return sortedByDate(articles: articles, sortDirection: sortDirection)
}
return articles
}
// MARK: -
private static func sortedByFeedName<T: SortableArticle>(articles: [T],
sortByDateDirection: ComparisonResult) -> [T] {
// Group articles by feed - feed ID is used to differentiate between
// two feeds that have the same name
var groupedArticles = Dictionary(grouping: articles) { "\($0.sortableName.lowercased())-\($0.sortableFeedID)" }
// Sort the articles within each group
for tuple in groupedArticles {
groupedArticles[tuple.key] = sortedByDate(articles: tuple.value,
sortDirection: sortByDateDirection)
}
// Flatten the articles dictionary back into an array sorted by feed name
var sortedArticles: [T] = []
for feedName in groupedArticles.keys.sorted() {
sortedArticles.append(contentsOf: groupedArticles[feedName] ?? [])
}
return sortedArticles
}
private static func isSortedByDate(_ lhs: SortableArticle,
_ rhs: SortableArticle,
sortDirection: ComparisonResult) -> Bool {
if lhs.sortableDate == rhs.sortableDate {
return lhs.sortableID < rhs.sortableID
private static func sortedByDate<T: SortableArticle>(articles: [T],
sortDirection: ComparisonResult) -> [T] {
let articles = articles.sorted { (article1, article2) -> Bool in
if article1.sortableDate == article2.sortableDate {
return article1.sortableArticleID < article2.sortableArticleID
}
if sortDirection == .orderedDescending {
return article1.sortableDate > article2.sortableDate
}
return article1.sortableDate < article2.sortableDate
}
if sortDirection == .orderedDescending {
return lhs.sortableDate > rhs.sortableDate
}
return lhs.sortableDate < rhs.sortableDate
return articles
}
}

View File

@ -14,96 +14,227 @@ import XCTest
class ArticleArrayTests: XCTestCase {
// MARK: sortByDate ascending tests
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 article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-60.0), sortableArticleID: "1", sortableFeedID: "4")
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(60.0), sortableArticleID: "2", sortableFeedID: "6")
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(120.0), sortableArticleID: "3", sortableFeedID: "6")
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-120.0), sortableArticleID: "4", sortableFeedID: "5")
let articles = [article1, article2, article3, article4, article5, article6]
let articles = [article1, article2, article3, article4]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedAscending,
groupByFeed: false)
XCTAssertEqual(sortedArticles, [article5, article4, article3, article1, article2, article6])
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article4)
XCTAssertEqual(sortedArticles.articleAtRow(1), article1)
XCTAssertEqual(sortedArticles.articleAtRow(2), article2)
XCTAssertEqual(sortedArticles.articleAtRow(3), article3)
}
func testSortByDateAscendingWithSameDate() {
let now = Date()
// Articles with the same date should end up being sorted by their article ID
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "3")
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableArticleID: "4", sortableFeedID: "4")
let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableArticleID: "5", sortableFeedID: "5")
let articles = [article1, article2, article3, article4, article5]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedAscending,
groupByFeed: false)
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article5)
XCTAssertEqual(sortedArticles.articleAtRow(1), article4)
XCTAssertEqual(sortedArticles.articleAtRow(2), article1)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article3)
}
func testSortByDateAscendingWithGroupByFeed() {
let now = Date()
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: Date(timeInterval: -100.0, since: now), sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "Jenny's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "2")
let article4 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -1000.0, since: now), sortableArticleID: "1", sortableFeedID: "3")
let article5 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "3")
let article6 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: 10.0, since: now), sortableArticleID: "3", sortableFeedID: "2")
let article7 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "1")
let article8 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "0")
let article9 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "0")
// 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]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles, sortDirection: .orderedAscending, groupByFeed: true)
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])
XCTAssertEqual(sortedArticles.count, 9)
// Gordy's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(0), article4)
XCTAssertEqual(sortedArticles.articleAtRow(1), article5)
// Jenny's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(2), article3)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article6)
// Phil's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(5), article1)
XCTAssertEqual(sortedArticles.articleAtRow(6), article7)
// Zippy's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(7), article8)
XCTAssertEqual(sortedArticles.articleAtRow(8), article9)
}
// MARK: sortByDate descending tests
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 article1 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-60.0), sortableArticleID: "1", sortableFeedID: "4")
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(60.0), sortableArticleID: "2", sortableFeedID: "6")
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now.addingTimeInterval(120.0), sortableArticleID: "3", sortableFeedID: "6")
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: now.addingTimeInterval(-120.0), sortableArticleID: "4", sortableFeedID: "5")
let articles = [article1, article2, article3, article4, article5, article6]
let articles = [article1, article2, article3, article4]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedDescending,
groupByFeed: false)
XCTAssertEqual(sortedArticles, [article6, article3, article1, article2, article4, article5])
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article3)
XCTAssertEqual(sortedArticles.articleAtRow(1), article2)
XCTAssertEqual(sortedArticles.articleAtRow(2), article1)
XCTAssertEqual(sortedArticles.articleAtRow(3), article4)
}
func testSortByDateDescendingWithSameDate() {
let now = Date()
// Articles with the same date should end up being sorted by their article ID
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "Matt's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Sally's Feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "3")
let article4 = TestArticle(sortableName: "Susie's Feed", sortableDate: Date(timeInterval: -60.0, since: now), sortableArticleID: "4", sortableFeedID: "4")
let article5 = TestArticle(sortableName: "Paul's Feed", sortableDate: Date(timeInterval: -120.0, since: now), sortableArticleID: "5", sortableFeedID: "5")
let articles = [article1, article2, article3, article4, article5]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedDescending,
groupByFeed: false)
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article1)
XCTAssertEqual(sortedArticles.articleAtRow(1), article2)
XCTAssertEqual(sortedArticles.articleAtRow(2), article3)
XCTAssertEqual(sortedArticles.articleAtRow(3), article4)
XCTAssertEqual(sortedArticles.articleAtRow(4), article5)
}
func testSortByDateDescendingWithGroupByFeed() {
let now = Date()
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: Date(timeInterval: -100.0, since: now), sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "Jenny's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "2")
let article4 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -1000.0, since: now), sortableArticleID: "1", sortableFeedID: "3")
let article5 = TestArticle(sortableName: "Gordy's Blog", sortableDate: Date(timeInterval: -10.0, since: now), sortableArticleID: "2", sortableFeedID: "3")
let article6 = TestArticle(sortableName: "Jenny's Feed", sortableDate: Date(timeInterval: 10.0, since: now), sortableArticleID: "3", sortableFeedID: "2")
let article7 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "1")
let article8 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "0")
let article9 = TestArticle(sortableName: "Zippy's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "0")
// 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]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles, sortDirection: .orderedDescending, groupByFeed: true)
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])
XCTAssertEqual(sortedArticles.count, 9)
// Gordy's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(0), article5)
XCTAssertEqual(sortedArticles.articleAtRow(1), article4)
// Jenny's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(2), article6)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article3)
// Phil's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(5), article7)
XCTAssertEqual(sortedArticles.articleAtRow(6), article1)
// Zippy's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(7), article8)
XCTAssertEqual(sortedArticles.articleAtRow(8), article9)
}
// MARK: Additional group by feed tests
func testGroupByFeedWithCaseInsensitiveFeedNames() {
let now = Date()
let article1 = TestArticle(sortableName: "phil's feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "1")
let article2 = TestArticle(sortableName: "PhIl's FEed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "1")
let article3 = TestArticle(sortableName: "APPLE's feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "2")
let article4 = TestArticle(sortableName: "PHIL'S FEED", sortableDate: now, sortableArticleID: "4", sortableFeedID: "1")
let article5 = TestArticle(sortableName: "apple's feed", sortableDate: now, sortableArticleID: "5", sortableFeedID: "2")
let articles = [article1, article2, article3, article4, article5]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedAscending,
groupByFeed: true)
XCTAssertEqual(sortedArticles.count, articles.count)
// Apple's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(0), article3)
XCTAssertEqual(sortedArticles.articleAtRow(1), article5)
// Phil's feed articles
XCTAssertEqual(sortedArticles.articleAtRow(2), article1)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article4)
}
func testGroupByFeedWithSameFeedNames() {
let now = Date()
// Articles with the same feed name should be sorted by feed ID
let article1 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "1", sortableFeedID: "2")
let article2 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "2", sortableFeedID: "2")
let article3 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "3", sortableFeedID: "1")
let article4 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "4", sortableFeedID: "2")
let article5 = TestArticle(sortableName: "Phil's Feed", sortableDate: now, sortableArticleID: "5", sortableFeedID: "1")
let articles = [article1, article2, article3, article4, article5]
let sortedArticles = ArticleSorter.sortedByDate(articles: articles,
sortDirection: .orderedAscending,
groupByFeed: true)
XCTAssertEqual(sortedArticles.count, articles.count)
XCTAssertEqual(sortedArticles.articleAtRow(0), article3)
XCTAssertEqual(sortedArticles.articleAtRow(1), article5)
XCTAssertEqual(sortedArticles.articleAtRow(2), article1)
XCTAssertEqual(sortedArticles.articleAtRow(3), article2)
XCTAssertEqual(sortedArticles.articleAtRow(4), article4)
}
}
private struct TestArticle: SortableArticle, Equatable {
let sortableName: String
let sortableDate: Date
let sortableID: String
let sortableArticleID: String
let sortableFeedID: String
}
private extension Array where Element == TestArticle {
func articleAtRow(_ row: Int) -> TestArticle? {
if row < 0 || row == NSNotFound || row > count - 1 {
return nil
}
return self[row]
}
}