git.iko.soy/parseable-macos
on master help sign in
Fix 2 bugs: unstable sort reshuffles tied rows, negative zero displays as -0
96828aciko
ParseableViewer/Models/JSONValue.swift+3
@@ -55,6 +55,9 @@ enum JSONValue: Codable, Hashable, Sendable, Comparable {
55 // letting String(_:) emit "inf"/"-inf"/"nan" into the UI and CSV cells.
55 // letting String(_:) emit "inf"/"-inf"/"nan" into the UI and CSV cells.
56 if !v.isFinite { return "null" }
56 if !v.isFinite { return "null" }
57 if v == v.rounded() && abs(v) < 1e15 {
57 if v == v.rounded() && abs(v) < 1e15 {
58+ // Normalize negative zero so it renders as "0", not "-0"
59+ // (`%.0f` preserves the sign bit of -0.0).
60+ if v == 0 { return "0" }
58 return String(format: "%.0f", v)
61 return String(format: "%.0f", v)
59 }
62 }
60 return String(v)
63 return String(v)
ParseableViewer/Views/LiveTailView.swift+9−5
@@ -322,11 +322,15 @@ struct LiveTailView: View {
322 try? await Task.sleep(for: .milliseconds(150))
322 try? await Task.sleep(for: .milliseconds(150))
323 guard !Task.isCancelled else { return }
323 guard !Task.isCancelled else { return }
324 let sorted = await Task.detached(priority: .userInitiated) {
324 let sorted = await Task.detached(priority: .userInitiated) {
325+ // `sorted(by:)` is not guaranteed stable and entry ids are random
326+ // UUIDs, so break ties on arrival order (the buffer index) to stop
327+ // equal-keyed rows from reshuffling on every poll.
325− entries.sorted { a, b in
328+ entries.enumerated().sorted { a, b in
326− let aVal = a.record[col] ?? .null
329+ let aVal = a.element.record[col] ?? .null
327− let bVal = b.record[col] ?? .null
330+ let bVal = b.element.record[col] ?? .null
328− return asc ? aVal < bVal : bVal < aVal
331+ if aVal != bVal { return asc ? aVal < bVal : bVal < aVal }
329− }
332+ return a.offset < b.offset
333+ }.map(\.element)
330 }.value
334 }.value
331 guard !Task.isCancelled else { return }
335 guard !Task.isCancelled else { return }
332 cachedSorted = sorted
336 cachedSorted = sorted
ParseableViewer/Views/LogTableView.swift+9−2
@@ -359,7 +359,12 @@ struct LogTableView: View {
359 sorted = enumerated.sorted { a, b in
359 sorted = enumerated.sorted { a, b in
360 let aVal = a.element[col] ?? .null
360 let aVal = a.element[col] ?? .null
361 let bVal = b.element[col] ?? .null
361 let bVal = b.element[col] ?? .null
362+ // `sorted(by:)` is not guaranteed stable, so break ties on
363+ // the original index to keep equal-keyed rows in a fixed
364+ // order across re-sorts (otherwise low-cardinality columns
365+ // like `level`/`status` would reshuffle on every refresh).
362− return asc ? aVal < bVal : bVal < aVal
366+ if aVal != bVal { return asc ? aVal < bVal : bVal < aVal }
367+ return a.offset < b.offset
363 }
368 }
364 } else {
369 } else {
365 sorted = enumerated
370 sorted = enumerated
@@ -403,7 +408,9 @@ struct LogTableView: View {
403 sorted = enumerated.sorted { a, b in
408 sorted = enumerated.sorted { a, b in
404 let aVal = a.element[sortColumn] ?? .null
409 let aVal = a.element[sortColumn] ?? .null
405 let bVal = b.element[sortColumn] ?? .null
410 let bVal = b.element[sortColumn] ?? .null
411+ // Stable tiebreak on original index — see the async path above.
406− return sortAscending ? aVal < bVal : bVal < aVal
412+ if aVal != bVal { return sortAscending ? aVal < bVal : bVal < aVal }
413+ return a.offset < b.offset
407 }
414 }
408 } else {
415 } else {
409 sorted = enumerated
416 sorted = enumerated