Better case-insensitive sorting for lists

This commit is contained in:
Shadowfacts 2022-12-01 18:26:48 -05:00
parent c9fa11cc3b
commit 4ca57f8c76
4 changed files with 96 additions and 18 deletions

View File

@ -53,6 +53,8 @@
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */; };
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759A29384F9C00C0B37F /* FilterMO.swift */; };
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */; };
D61F75A129396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */; };
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
@ -422,6 +424,8 @@
D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowedHashtag.swift; sourceTree = "<group>"; };
D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleFollowHashtagService.swift; sourceTree = "<group>"; };
D61F759A29384F9C00C0B37F /* FilterMO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterMO.swift; sourceTree = "<group>"; };
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparator.swift; sourceTree = "<group>"; };
D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemiCaseSensitiveComparatorTests.swift; sourceTree = "<group>"; };
D620483323D3801D008A63EF /* LinkTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkTextView.swift; sourceTree = "<group>"; };
D620483523D38075008A63EF /* ContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentTextView.swift; sourceTree = "<group>"; };
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
@ -1424,6 +1428,7 @@
D6B81F432560390300F6E31D /* MenuController.swift */,
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */,
D6895DE828D962C2006341DA /* TimelineLikeController.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
D6DFC69F242C4CCC00ACC392 /* Weak.swift */,
@ -1454,6 +1459,7 @@
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */,
D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */,
D6114E1627F8BB210080E273 /* VersionTests.swift */,
D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */,
D6D4DDE6212518A200E1C4BB /* Info.plist */,
);
path = TuskerTests;
@ -2006,6 +2012,7 @@
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
@ -2078,6 +2085,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D61F75A129396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift in Sources */,
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */,
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */,
D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */,

View File

@ -304,7 +304,7 @@ class MastodonController: ObservableObject {
run(req) { response in
if case .success(let lists, _) = response {
DispatchQueue.main.async {
self.lists = lists.sorted(using: ListComparator())
self.lists = lists.sorted(using: SemiCaseSensitiveComparator.keyPath(\.title))
}
}
}
@ -314,7 +314,7 @@ class MastodonController: ObservableObject {
func addedList(_ list: List) {
var new = self.lists
new.append(list)
new.sort { $0.title < $1.title }
new.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
self.lists = new
}
@ -329,7 +329,7 @@ class MastodonController: ObservableObject {
if let index = new.firstIndex(where: { $0.id == list.id }) {
new[index] = list
}
new.sort(using: ListComparator())
new.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
self.lists = new
}
@ -367,18 +367,3 @@ class MastodonController: ObservableObject {
}
}
private struct ListComparator: SortComparator {
typealias Compared = List
var underlying = String.Comparator(options: .caseInsensitive)
var order: SortOrder {
get { underlying.order }
set { underlying.order = newValue }
}
func compare(_ lhs: List, _ rhs: List) -> ComparisonResult {
return underlying.compare(lhs.title, rhs.title)
}
}

View File

@ -0,0 +1,61 @@
//
// SemiCaseSensitiveComparator.swift
// Tusker
//
// Created by Shadowfacts on 11/30/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import Foundation
/// A comparator that sorts objects with a string key path case insensitively unless they're the same, in which case uppercase comes after lowercase.
struct SemiCaseSensitiveComparator: SortComparator {
var order: SortOrder = .forward
typealias Compared = String
static func keyPath<Object>(_ keyPath: KeyPath<Object, String>) -> KeyPathComparator<Object> {
return KeyPathComparator(keyPath, comparator: SemiCaseSensitiveComparator())
}
func compare(_ lhs: String, _ rhs: String) -> ComparisonResult {
let result = doCompare(lhs, rhs)
if case .reverse = order {
switch result {
case .orderedDescending:
return .orderedAscending
case .orderedAscending:
return .orderedDescending
case .orderedSame:
return .orderedSame
}
} else {
return result
}
}
private func doCompare(_ lhs: String, _ rhs: String) -> ComparisonResult {
for (l, r) in zip(lhs, rhs) {
let lLower = l.lowercased()
let rLower = r.lowercased()
if lLower < rLower {
return .orderedAscending
} else if lLower > rLower {
return .orderedDescending
} else {
if l < r {
return .orderedDescending
} else if l > r {
return .orderedAscending
}
}
}
if lhs.count > rhs.count {
return .orderedDescending
} else if lhs.count < rhs.count {
return .orderedAscending
} else {
return .orderedSame
}
}
}

View File

@ -0,0 +1,24 @@
//
// SemiCaseSensitiveComparatorTests.swift
// TuskerTests
//
// Created by Shadowfacts on 12/1/22.
// Copyright © 2022 Shadowfacts. All rights reserved.
//
import XCTest
@testable import Tusker
final class SemiCaseSensitiveComparatorTests: XCTestCase {
func testCompare() {
let comparator = SemiCaseSensitiveComparator()
XCTAssertEqual(comparator.compare("a", "a"), .orderedSame)
XCTAssertEqual(comparator.compare("a", "A"), .orderedAscending)
XCTAssertEqual(comparator.compare("A", "a"), .orderedDescending)
XCTAssertEqual(comparator.compare("a", "B"), .orderedAscending)
XCTAssertEqual(comparator.compare("b", "A"), .orderedDescending)
XCTAssertEqual(["TEST", "Test", "test"].sorted(using: comparator), ["test", "Test", "TEST"])
}
}