iOS-Prozente auf dem Cloud-Mac mit dem Größte-Reste-Verfahren prüfen
Eine iOS-Statistikseite zeigt für drei identische Gewichte jeweils 33 % an – zusammen sind das aber nur 99 %. Wird die gesamte Differenz dem letzten Eintrag zugeschlagen, kann wiederum ein Eintrag mit Gewicht null einen Prozentanteil erhalten. In einer Build-Pipeline auf dem Cloud-Mac muss dafür kein Simulator gestartet werden: Wird der Verteilungsalgorithmus als reine Swift-Funktion umgesetzt, lassen sich Summe, Reihenfolge und ungültige Eingaben prüfen. Die Oberfläche verwendet anschließend dasselbe Ergebnis.
Zuerst Anzeigeeinheit und Eingabevertrag festlegen
Hier geht es um nichtnegative ganzzahlige Zählwerte, etwa die Anzahl abgeschlossener Aufgaben in drei Kategorien – nicht um Geldbeträge, Gleitkomma-Messwerte oder bereits gerundete Prozentangaben. Bei insgesamt 100 Anzeigeeinheiten steht jede Einheit für einen Prozentpunkt. Für eine Nachkommastelle wird die Gesamtzahl auf 1000 erhöht; eine Einheit entspricht dann einem Zehntelprozentpunkt.
Der Vertrag umfasst vier Regeln: Die Ausgabe ist genauso lang wie die Eingabe; alle Ausgabewerte sind nichtnegativ und ergeben zusammen die Gesamtzahl der Einheiten; ein Gewicht von null erhält null Einheiten; dieselbe Eingabe liefert immer dasselbe Ergebnis. Ein leeres Array, negative Gewichte oder eine Gewichtssumme von null führen unmittelbar zu einem Fehler. Eine Gleichverteilung wird nicht eigenmächtig erzeugt.
„Keine Daten“ und „alle Einträge haben denselben Anteil“ sind zwei unterschiedliche fachliche Zustände. Besteht die Eingabe nur aus Nullen, muss die Seite einen Leerzustand anzeigen. Der Verteilungsalgorithmus darf daraus keine scheinbar gültige Statistik machen.
Bei gleichen Resten entscheidet der Eingabeindex über die Reihenfolge. Der aufrufende Code muss die Daten deshalb vorher anhand stabiler Kategoriekennungen sortieren. Die Iterationsreihenfolge eines Dictionary darf nicht zur Produktregel werden. Andernfalls könnten nach einer Aktualisierung andere Einträge die Ausgleichseinheiten erhalten.
Die Differenz mit dem ganzzahligen Größte-Reste-Verfahren ausgleichen
Zunächst wird jedes Gewicht mit der Gesamtzahl der Einheiten multipliziert und das Produkt durch die Gewichtssumme geteilt. Der Quotient ergibt die Grundzuteilung, der Rest bestimmt die Reihenfolge für den Ausgleich. Die zur Gesamtsumme fehlenden Einheiten werden einzeln vergeben, beginnend mit dem größten Rest.
Da jeder Quotient abgerundet wird, ist die Zahl der noch zu vergebenden Einheiten stets kleiner als die Anzahl der Einträge. Weder eine Schleife bis zur Konvergenz noch eine Gleitkommatoleranz ist nötig. Speichern Sie den folgenden Code als percentage-check.swift. Er enthält die Implementierung und die grundlegenden Prüfungen.
enum AllocationError: Error {
case invalidInput
case overflow
}
func allocate(_ weights: [Int], units: Int = 100) throws -> [Int] {
guard !weights.isEmpty,
(1...10_000).contains(units),
weights.allSatisfy({ $0 >= 0 }) else {
throw AllocationError.invalidInput
}
var total = 0
for weight in weights {
let sum = total.addingReportingOverflow(weight)
guard !sum.overflow else {
throw AllocationError.overflow
}
total = sum.partialValue
}
guard total > 0 else {
throw AllocationError.invalidInput
}
var result = [Int]()
var remainders = [Int]()
for weight in weights {
let product = weight.multipliedReportingOverflow(by: units)
guard !product.overflow else {
throw AllocationError.overflow
}
result.append(product.partialValue / total)
remainders.append(product.partialValue % total)
}
let remaining = units - result.reduce(0, +)
let order = weights.indices.sorted {
if remainders[$0] == remainders[$1] {
return $0 < $1
}
return remainders[$0] > remainders[$1]
}
for index in order.prefix(remaining) {
result[index] += 1
}
return result
}
func expectFailure(_ weights: [Int], units: Int = 100) {
do {
_ = try allocate(weights, units: units)
} catch {
return
}
preconditionFailure("Expected allocation to fail")
}
let equal = try allocate([1, 1, 1])
precondition(equal == [34, 33, 33])
let withZero = try allocate([0, 1, 1])
precondition(withZero == [0, 50, 50])
let tenths = try allocate([1, 1, 1], units: 1000)
precondition(tenths == [334, 333, 333])
for a in 0...12 {
for b in 0...12 {
for c in 0...12 {
let weights = [a, b, c]
let total = weights.reduce(0, +)
if total == 0 { continue }
let output = try allocate(weights)
precondition(output.count == weights.count)
precondition(output.reduce(0, +) == 100)
precondition(output.allSatisfy { $0 >= 0 })
for index in weights.indices {
let floorQuota = weights[index] * 100 / total
precondition(
output[index] == floorQuota ||
output[index] == floorQuota + 1
)
if weights[index] == 0 {
precondition(output[index] == 0)
}
}
}
}
}
expectFailure([])
expectFailure([0, 0])
expectFailure([-1, 2])
expectFailure([1], units: 0)
expectFailure([Int.max, 1])
expectFailure([Int.max])
Der Code prüft bewusst sowohl bei der Summenbildung als auch bei der Multiplikation auf Überlauf. Selbst wenn der endgültige Prozentwert klein ist, kann das zwischenzeitliche Produkt den Ganzzahlbereich überschreiten. Hier wird die Eingabe abgelehnt, statt eine veränderte Genauigkeit durch die Umwandlung in Gleitkommazahlen zu verdecken. Sollen größere Zählwerte zulässig sein, muss dafür eine separate Lösung mit breiteren Ganzzahltypen entworfen werden.
Die eigenständige Prüfung auf dem Cloud-Mac ausführen
Prüfen Sie zunächst mit xcrun swift --version die aktive Toolchain und führen Sie anschließend die folgenden Befehle aus. Bei erfolgreichen Prüfungen erscheint keine Ausgabe. Eine fehlgeschlagene Zusicherung oder ein unbehandelter Fehler lässt den Prozess fehlschlagen; die Pipeline muss diesen Exitstatus beibehalten.
set -eu
xcrun swift percentage-check.swift
Für eine verbindliche Freigabeprüfung können Sie den Code mit xcrun swiftc -Onone kompilieren und anschließend ausführen. Ein Optimierungsmodus, der diese Zusicherungen deaktiviert, darf nicht verwendet werden. Langfristig ist es robuster, die Funktion in das Fachmodul zu übernehmen, die Zusicherungen in ein Test-Target zu verschieben und sie über den vorhandenen Testbefehl auszuführen. So müssen Skript und Produkt nicht jeweils eine eigene Version des Algorithmus pflegen.
Hier werden alle Kombinationen von drei Gewichten innerhalb eines kleinen Wertebereichs durchlaufen. Das ist kein Performance-Benchmark. Abgedeckt werden Konstellationen nahe der reinen Nulleingabe, die vollständige Zuteilung an einen einzigen Eintrag, gleiche Reste und kleine Gewichte, die auf null abgerundet werden. Zusätzliche Tests sollten die Eingabeformen erweitern, statt lediglich dieselbe gleichförmige Stichprobe zu wiederholen.
Rundungseffekte von fachlichen Regeln trennen
Besonders leicht wird übersehen, dass beim Sortieren die Zuordnung zu den ursprünglichen Einträgen verloren gehen kann. Die Implementierung sortiert nur die Indizes; das Ergebnisarray entspricht weiterhin den ursprünglichen Einträgen. Die Oberfläche darf die Rangfolge der Reste daher nicht direkt als Anzeigereihenfolge der Kategorien übernehmen.
Verlangen Sie außerdem nicht, dass „jedes positive Gewicht mindestens eine Einheit erhält“. Gibt es mehr Kategorien als Anzeigeeinheiten, lässt sich diese Forderung nicht mit einer unveränderten Gesamtsumme vereinbaren. Sollen sehr kleine Anteile sichtbar sein, kann die Anzeigegenauigkeit erhöht oder ein gesonderter Hinweis wie „unterhalb der Anzeigeschwelle“ verwendet werden. Die Zuteilungen dürfen jedoch nicht stillschweigend verändert werden.
Schließlich bietet dieses Verfahren keine Monotoniegarantie über verschiedene Datensätze hinweg. Werden die Gewichte anderer Kategorien oder die Anzeigegenauigkeit geändert, kann sich die Rangfolge der Reste verschieben. Tests sollten die zulässigen Zuteilungsgrenzen für den aktuellen Datensatz prüfen, statt zu behaupten, dass der Anteil eines Eintrags über alle Aktualisierungen hinweg niemals sinkt.
Dezimalformatierung dient ausschließlich der Anzeige
Bei insgesamt 1000 Einheiten steht 334 für 33,4 %. Erwartet die Formatierungs-API einen Anteil zwischen null und eins, muss der Wert entsprechend 334 / 1000 übergeben werden – ohne ihn nochmals mit hundert zu multiplizieren. Bei der Formatierung dürfen weder die Anteile aus den ursprünglichen Gewichten neu berechnet noch die Ergebnisse erneut auf eine andere Genauigkeit gerundet werden.
Fehler durch prüfbare Nachweise nachvollziehbar machen
Halten Sie vor dem Merge die Eingabegewichte, die Gesamtzahl der Einheiten, die Grundzuteilungen, die Rangfolge der Reste und die endgültige Ausgabe fest. Können Produktionsstatistiken sensible Kategorien enthalten, reproduzieren Sie den Fall mit festen Test-Fixtures. Echte fachliche Detaildaten gehören nicht in die Build-Artefakte.
Im Review stehen drei Grenzfälle im Mittelpunkt: Führen ungültige Eingaben zu einem eindeutigen Fehler? Werden gleiche Reste stabil aufgelöst? Behält die Formatierung der Oberfläche die festgelegte Genauigkeit bei? Nach erfolgreicher Abnahme liefert die Datenschicht nur Gewichte, die Verteilungsschicht nur ganzzahlige Einheiten und die Darstellungsschicht übernimmt ausschließlich die lokalisierte Formatierung. Mit dieser Trennung lässt sich eine fehlerhafte Prozentsumme einem konkreten Verarbeitungsschritt zuordnen, statt durch einen Ausgleich beim letzten Eintrag nur den Anschein eines korrekten Ergebnisses zu wahren.
Häufig gestellte Fragen
Warum reicht einzelnes Runden nicht aus?
Unabhängiges Runden erhält die Summe nicht. Drei gleiche Gewichte mit jeweils 33 Prozent ergeben nur 99 Prozent. Das Verfahren verteilt zunächst ganze Quoten und ergänzt die übrigen Einheiten nach Restgröße.
Steigt der angezeigte Anteil bei höherem Gewicht immer?
Das ist über verschiedene Eingaben hinweg nicht garantiert. Andere Gewichte oder eine andere Einheitenzahl können die Rangfolge der Reste ändern. Geprüft werden hier Summe, Quotengrenzen und deterministische Ergebnisse.
Cloud-Mac mit eigener Pipeline testen
ArmRDP bietet dedizierte physische Mac mini statt virtueller Maschinen. Mieten Sie tageweise und prüfen Sie zuerst Toolchain und Build-Ergebnisse, bevor Sie die passende Laufzeit für Ihr Team wählen.
Cloud-Mac mieten