forked from shadowfacts/Tusker
Better case-insensitive sorting for lists
This commit is contained in:
parent
c9fa11cc3b
commit
4ca57f8c76
|
@ -53,6 +53,8 @@
|
||||||
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
|
D61F75942936F0DA00C0B37F /* FollowedHashtag.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75932936F0DA00C0B37F /* FollowedHashtag.swift */; };
|
||||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */; };
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F75952937037800C0B37F /* ToggleFollowHashtagService.swift */; };
|
||||||
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */ = {isa = PBXBuildFile; fileRef = D61F759A29384F9C00C0B37F /* FilterMO.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 */; };
|
D620483423D3801D008A63EF /* LinkTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483323D3801D008A63EF /* LinkTextView.swift */; };
|
||||||
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
D620483623D38075008A63EF /* ContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483523D38075008A63EF /* ContentTextView.swift */; };
|
||||||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D620483723D38190008A63EF /* StatusContentTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentTextView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1424,6 +1428,7 @@
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */,
|
D6B81F432560390300F6E31D /* MenuController.swift */,
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */,
|
||||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
|
||||||
|
D61F759E29385AD800C0B37F /* SemiCaseSensitiveComparator.swift */,
|
||||||
D6895DE828D962C2006341DA /* TimelineLikeController.swift */,
|
D6895DE828D962C2006341DA /* TimelineLikeController.swift */,
|
||||||
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
|
||||||
D6DFC69F242C4CCC00ACC392 /* Weak.swift */,
|
D6DFC69F242C4CCC00ACC392 /* Weak.swift */,
|
||||||
|
@ -1454,6 +1459,7 @@
|
||||||
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */,
|
D62FF04723D7CDD700909D6E /* AttributedStringHelperTests.swift */,
|
||||||
D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */,
|
D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */,
|
||||||
D6114E1627F8BB210080E273 /* VersionTests.swift */,
|
D6114E1627F8BB210080E273 /* VersionTests.swift */,
|
||||||
|
D61F75A029396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift */,
|
||||||
D6D4DDE6212518A200E1C4BB /* Info.plist */,
|
D6D4DDE6212518A200E1C4BB /* Info.plist */,
|
||||||
);
|
);
|
||||||
path = TuskerTests;
|
path = TuskerTests;
|
||||||
|
@ -2006,6 +2012,7 @@
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
||||||
|
D61F759F29385AD800C0B37F /* SemiCaseSensitiveComparator.swift in Sources */,
|
||||||
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
D627FF76217E923E00CC0648 /* DraftsManager.swift in Sources */,
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||||
|
@ -2078,6 +2085,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D61F75A129396DE200C0B37F /* SemiCaseSensitiveComparatorTests.swift in Sources */,
|
||||||
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */,
|
D62FF04823D7CDD700909D6E /* AttributedStringHelperTests.swift in Sources */,
|
||||||
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */,
|
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */,
|
||||||
D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */,
|
D6114E1727F8BB210080E273 /* VersionTests.swift in Sources */,
|
||||||
|
|
|
@ -304,7 +304,7 @@ class MastodonController: ObservableObject {
|
||||||
run(req) { response in
|
run(req) { response in
|
||||||
if case .success(let lists, _) = response {
|
if case .success(let lists, _) = response {
|
||||||
DispatchQueue.main.async {
|
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) {
|
func addedList(_ list: List) {
|
||||||
var new = self.lists
|
var new = self.lists
|
||||||
new.append(list)
|
new.append(list)
|
||||||
new.sort { $0.title < $1.title }
|
new.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||||
self.lists = new
|
self.lists = new
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ class MastodonController: ObservableObject {
|
||||||
if let index = new.firstIndex(where: { $0.id == list.id }) {
|
if let index = new.firstIndex(where: { $0.id == list.id }) {
|
||||||
new[index] = list
|
new[index] = list
|
||||||
}
|
}
|
||||||
new.sort(using: ListComparator())
|
new.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
|
||||||
self.lists = new
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue