git.iko.soy/parseable-macos
on master help sign in
Add stable sorting to stream list in sidebar (#29)

Streams are now sorted alphabetically using localizedStandardCompare, which provides natural ordering (e.g. stream2 before stream10) and case-insensitive sorting. LogStream conforms to Comparable so sorted() produces a stable, deterministic order regardless of API response order. https://claude.ai/code/session_01Ruj7GPEWnqufwxwsXCbp26 Co-authored-by: Claude <noreply@anthropic.com>

commitc70284fceeee30dccc88a90954ccb8dd8a49498e authoriko <mail@iko.soy> dateSun 15 Feb 2026, 19:49 (8 months ago) parenteb5d360
3 files changed+81−3
ParseableViewer/App/AppState.swift+2−2 ParseableViewer/Models/LogStream.swift+5−1 ParseableViewerTests/LogStreamTests.swift+74
ParseableViewer/App/AppState.swift+2−2
@@ -61,9 +61,9 @@ final class AppState {
6161
6262 var filteredStreams: [LogStream] {
6363 if streamSearchText.isEmpty {
64− return streams
64+ return streams.sorted()
6565 }
66− return streams.filter { $0.name.localizedCaseInsensitiveContains(streamSearchText) }
66+ return streams.filter { $0.name.localizedCaseInsensitiveContains(streamSearchText) }.sorted()
6767 }
6868
6969 deinit {
ParseableViewer/Models/LogStream.swift+5−1
@@ -1,6 +1,6 @@
11import Foundation
22
3−struct LogStream: Identifiable, Codable, Hashable, Sendable {
3+struct LogStream: Identifiable, Codable, Hashable, Sendable, Comparable {
44 let name: String
55
66 var id: String { name }
@@ -9,6 +9,10 @@ struct LogStream: Identifiable, Codable, Hashable, Sendable {
99 self.name = name
1010 }
1111
12+ static func < (lhs: LogStream, rhs: LogStream) -> Bool {
13+ lhs.name.localizedStandardCompare(rhs.name) == .orderedAscending
14+ }
15+
1216 init(from decoder: Decoder) throws {
1317 // Handle both {"name": "x"} and plain string formats
1418 if let container = try? decoder.container(keyedBy: CodingKeys.self) {
ParseableViewerTests/LogStreamTests.swiftnew file+74
@@ -0,0 +1,74 @@
1+import XCTest
2+@testable import ParseableViewer
3+
4+final class LogStreamTests: XCTestCase {
5+ // MARK: - Comparable
6+
7+ func testSortsByNameAlphabetically() {
8+ let streams = [
9+ LogStream(name: "zebra"),
10+ LogStream(name: "alpha"),
11+ LogStream(name: "middle"),
12+ ]
13+ let sorted = streams.sorted()
14+ XCTAssertEqual(sorted.map(\.name), ["alpha", "middle", "zebra"])
15+ }
16+
17+ func testSortIsCaseInsensitive() {
18+ let streams = [
19+ LogStream(name: "Banana"),
20+ LogStream(name: "apple"),
21+ LogStream(name: "Cherry"),
22+ ]
23+ let sorted = streams.sorted()
24+ XCTAssertEqual(sorted.map(\.name), ["apple", "Banana", "Cherry"])
25+ }
26+
27+ func testSortWithNumericSuffixes() {
28+ // localizedStandardCompare sorts "stream2" before "stream10"
29+ let streams = [
30+ LogStream(name: "stream10"),
31+ LogStream(name: "stream2"),
32+ LogStream(name: "stream1"),
33+ ]
34+ let sorted = streams.sorted()
35+ XCTAssertEqual(sorted.map(\.name), ["stream1", "stream2", "stream10"])
36+ }
37+
38+ func testSortStability() {
39+ // Two streams with the same name should preserve their relative order
40+ // (Swift's sorted() is stable since Swift 5)
41+ let a = LogStream(name: "same")
42+ let b = LogStream(name: "same")
43+ let sorted = [a, b].sorted()
44+ XCTAssertEqual(sorted.count, 2)
45+ XCTAssertEqual(sorted[0].name, "same")
46+ XCTAssertEqual(sorted[1].name, "same")
47+ }
48+
49+ func testSortEmptyArray() {
50+ let streams: [LogStream] = []
51+ let sorted = streams.sorted()
52+ XCTAssertTrue(sorted.isEmpty)
53+ }
54+
55+ func testSortSingleElement() {
56+ let streams = [LogStream(name: "only")]
57+ let sorted = streams.sorted()
58+ XCTAssertEqual(sorted.map(\.name), ["only"])
59+ }
60+
61+ // MARK: - Decoding
62+
63+ func testDecodeFromObject() throws {
64+ let json = Data(#"{"name": "test-stream"}"#.utf8)
65+ let stream = try JSONDecoder().decode(LogStream.self, from: json)
66+ XCTAssertEqual(stream.name, "test-stream")
67+ }
68+
69+ func testDecodeFromString() throws {
70+ let json = Data(#""plain-string""#.utf8)
71+ let stream = try JSONDecoder().decode(LogStream.self, from: json)
72+ XCTAssertEqual(stream.name, "plain-string")
73+ }
74+}