Files
lanework/KanbanTests/RanksTests.swift
T
rzen 4e016fe8fe Implement Ranks — gapped fractional ordering
Pure ordering math: append at max+1024, head-insert at min−1024,
midpoint insertion with precision-exhaustion detection (nil on ties and
rounding onto an endpoint), renumbering to whole multiples of 1024, the
shared display-order tie-break (order, then folder name), and
tombstone-excluding overloads. 18 tests.

Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
2026-07-26 15:12:26 -04:00

159 lines
5.5 KiB
Swift

import Testing
@testable import Kanban
struct RanksTests {
// MARK: - Append
@Test func appendOnEmptyLaneReturnsBoardConvention() {
#expect(Ranks.append(toVisible: [Double]()) == 1024)
}
@Test func appendReturnsMaxPlusGap() {
#expect(Ranks.append(toVisible: [1024, 2048, 512]) == 3072)
}
// MARK: - Insert at head
@Test func insertAtHeadOnEmptyLaneReturnsBoardConvention() {
#expect(Ranks.insertAtHead(ofVisible: [Double]()) == 1024)
}
@Test func insertAtHeadReturnsMinMinusGap() {
#expect(Ranks.insertAtHead(ofVisible: [1024, 2048, 512]) == -512)
}
// MARK: - Midpoint
@Test func midpointBetweenDistinctValuesIsStrictlyBetween() throws {
let mid = try #require(Ranks.midpoint(between: 1024, and: 2048))
#expect(mid == 1536)
#expect(mid > 1024 && mid < 2048)
}
@Test func midpointIsOrderIndependent() {
let forward = Ranks.midpoint(between: 1024, and: 2048)
let reversed = Ranks.midpoint(between: 2048, and: 1024)
#expect(forward == reversed)
}
@Test func midpointBetweenEqualValuesReturnsNil() {
#expect(Ranks.midpoint(between: 42, and: 42) == nil)
}
@Test func midpointNeverEscapesTheOpenInterval() {
// Values close enough that a naive (a+b)/2 could round to one of the
// endpoints; the result (if any) must stay strictly inside.
let a = 1.0
let b = 1.0.nextUp.nextUp.nextUp
if let mid = Ranks.midpoint(between: a, and: b) {
#expect(mid > a && mid < b)
}
}
// MARK: - Renumbered
@Test func renumberedProducesWholeMultiplesOf1024() {
#expect(Ranks.renumbered(count: 4) == [1024, 2048, 3072, 4096])
}
@Test func renumberedWithZeroCountIsEmpty() {
#expect(Ranks.renumbered(count: 0) == [])
}
// MARK: - Display order / tie-break
@Test func sortedForDisplayOrdersAscendingByRank() {
let items: [(order: Double, name: String)] = [
(order: 3072, name: "c-card"),
(order: 1024, name: "a-card"),
(order: 2048, name: "b-card"),
]
let sorted = Ranks.sortedForDisplay(items, order: { $0.order }, name: { $0.name })
#expect(sorted.map { $0.name } == ["a-card", "b-card", "c-card"])
}
@Test func sortedForDisplayBreaksTiesByFolderName() {
let items: [(order: Double, name: String)] = [
(order: 1024, name: "zzz-uuid"),
(order: 1024, name: "aaa-uuid"),
(order: 1024, name: "mmm-uuid"),
]
let sorted = Ranks.sortedForDisplay(items, order: { $0.order }, name: { $0.name })
#expect(sorted.map { $0.name } == ["aaa-uuid", "mmm-uuid", "zzz-uuid"])
}
@Test func isOrderedForDisplayMatchesSortedForDisplay() {
let a: (order: Double, name: String) = (order: 1024, name: "aaa")
let b: (order: Double, name: String) = (order: 1024, name: "bbb")
#expect(Ranks.isOrderedForDisplay(a, before: b, order: { $0.order }, name: { $0.name }))
#expect(!Ranks.isOrderedForDisplay(b, before: a, order: { $0.order }, name: { $0.name }))
}
// MARK: - Tombstone exclusion
@Test func appendIgnoresTombstonedSiblings() {
let items: [(order: Double, isDeleted: Bool)] = [
(order: 1024, isDeleted: false),
(order: 9999, isDeleted: true),
(order: 2048, isDeleted: false),
]
#expect(Ranks.append(toVisible: items) == 3072)
}
@Test func insertAtHeadIgnoresTombstonedSiblings() {
let items: [(order: Double, isDeleted: Bool)] = [
(order: 1024, isDeleted: false),
(order: -9999, isDeleted: true),
(order: 2048, isDeleted: false),
]
#expect(Ranks.insertAtHead(ofVisible: items) == 0)
}
@Test func appendWithAllSiblingsTombstonedReturnsBoardConvention() {
let items: [(order: Double, isDeleted: Bool)] = [
(order: 1024, isDeleted: true),
(order: 2048, isDeleted: true),
]
#expect(Ranks.append(toVisible: items) == 1024)
}
@Test func insertAtHeadWithAllSiblingsTombstonedReturnsBoardConvention() {
let items: [(order: Double, isDeleted: Bool)] = [
(order: 1024, isDeleted: true),
(order: 2048, isDeleted: true),
]
#expect(Ranks.insertAtHead(ofVisible: items) == 1024)
}
// MARK: - Precision exhaustion → renumber, deterministically
@Test func precisionExhaustionThenRenumberIsDeterministic() {
func runScenario() -> (iterations: Int, renumbered: [Double]) {
let lower = 1024.0
var upper = 2048.0
var iterations = 0
let iterationCap = 4000
while iterations < iterationCap, let mid = Ranks.midpoint(between: lower, and: upper) {
upper = mid
iterations += 1
}
return (iterations, Ranks.renumbered(count: 5))
}
let first = runScenario()
let second = runScenario()
// Precision must actually have been exhausted, not just hit the cap.
#expect(first.iterations > 0)
#expect(first.iterations < 4000)
// Same scenario, run twice, must produce bit-identical results.
#expect(first.iterations == second.iterations)
#expect(first.renumbered == second.renumbered)
// Renumbering yields clean whole multiples of 1024.
#expect(first.renumbered == [1024, 2048, 3072, 4096, 5120])
}
}