Um eine berechnete Rastergröße in Swift in eine Ganzzahl umzuwandeln, wähle zuerst die Rundungsregel und verwende dann Int(exactly:), um die Darstellbarkeit zu prüfen. Runde bei einer mit Kacheln belegten Mac-Zeichenfläche jede Achse auf und prüfe, ob die abschließende Multiplikation überläuft.
Dieser Artikel entwickelt die Berechnung für ein Desktop-Zeichenprogramm: Für eine rechteckige Zeichenfläche und Kacheln fester Größe sollen die benötigten Spalten, Zeilen und Kacheln insgesamt bestimmt werden. Alle folgenden Koordinaten und Anzahlen sind synthetisch. Das Raster beginnt am Ursprung der Zeichenfläche, hat keine Abstände und lässt an den äußeren Rändern teilweise Kacheln zu.
Die Rundungsregel nach dem Zweck des Rasters auswählen
Für eine gebrochene Rasterdimension muss zuerst eine Regel festgelegt werden, bevor sie in einen Typ umgewandelt wird. Ist eine Zeichenfläche 650 Punkte breit und eine Kachel 64 Punkte breit, ergibt die berechnete Dimension 650.0 / 64.0, also 10.15625. Um die gesamte Breite abzudecken, sind 11 Spalten erforderlich. Werden nur vollständige Kacheln gezählt, die in diese Breite passen, sind es 10.
Swift bietet für diese unterschiedlichen Aufgaben explizite Rundungsregeln. .up rundet in Richtung positive Unendlichkeit, .down in Richtung negative Unendlichkeit und .towardZero entfernt den Nachkommateil. Für das Runden auf den nächsten Wert gibt es eigene Regeln für Gleichstände. Diese Verhaltensweisen sind in Apples Dokumentation zu FloatingPointRoundingRule definiert.
| Aufgabe | Regel für eine nichtnegative Achsendimension | Ergebnis für 10.15625 |
|---|---|---|
| Die gesamte Ausdehnung einschließlich einer Randkachel abdecken | .up |
11.0 |
| Vollständige Kacheln zählen, die in die Ausdehnung passen | .down |
10.0 |
| Die nächstgelegene ganze Dimension wählen | .toNearestOrAwayFromZero |
10.0 |
| Verlangen, dass die berechnete Dimension bereits ganzzahlig ist | Nicht runden; direkt Int(exactly:) versuchen |
nil |
Für die Abdeckung verwendest du .rounded(.up). Ein Aufruf von .rounded() ohne Regel wählt die Rundung auf den nächsten Wert mit Gleichständen weg von null; dadurch kann ein Teil dieser Zeichenfläche unbedeckt bleiben. Außerdem liefert das Runden eines Double wieder einen ganzzahligen Double-Wert; die Umwandlung in eine Ganzzahl ist ein separater Schritt. Siehe Apples Dokumentation zu rounded(_:).
Spalten und Zeilen getrennt runden
Angenommen, die synthetische Zeichenfläche reicht horizontal von Koordinate 20 bis 670 und vertikal von 30 bis 440. Sie ist 650 Punkte breit und 410 Punkte hoch. Bei quadratischen Kacheln mit einer Seitenlänge von 64 Punkten lautet die Abdeckungsberechnung:
- Spalten:
ceil(650 / 64) = 11. - Zeilen:
ceil(410 / 64) = 7. - Insgesamt:
11 × 7 = 77Kacheln.
Dies sind veranschaulichende Rechenergebnisse, die sich aus dem angegebenen Layout ergeben. Wird die Fläche durch die Kachelfläche geteilt und das Ergebnis einmal gerundet, erhält man 66. Das beschreibt nicht die benötigte rechteckige Anordnung. Jede Zeile benötigt 11 Kachelpositionen, und die Zeichenfläche benötigt sieben solcher Zeilen. Behalte diese beiden Dimensionen bei, bis beide gerundet sind.
Diese Formel setzt voraus, dass die erste Kachel am Ursprung der Zeichenfläche beginnt. Gehören die Kacheln stattdessen zu einem Raster mit einem anderen Ursprung, kann der Startversatz beeinflussen, wie viele Zellen das Rechteck schneidet. Lege diese Ausrichtung fest, bevor du eine Berechnung nur anhand der Breite anwendest.
Warum eine direkte Ganzzahlumwandlung nicht ausreicht
Swifts gewöhnlicher Initialisierer zur Umwandlung von Gleitkommazahlen in Ganzzahlen rundet in Richtung null. Daher ergibt Int(10.15625) den Wert 10. Außerdem muss der Quellwert nach dem Abschneiden in den Ziel-Ganzzahltyp passen; laut Apples Dokumentation kann eine Umwandlung außerhalb des Wertebereichs einen Laufzeitfehler verursachen. Siehe BinaryInteger.init(_:).
Für eine berechnete Abdeckungsdimension ist daher die folgende Reihenfolge sinnvoll: Int(exactly: value.rounded(.up)), gefolgt von einer optionalen Behandlung. Apple dokumentiert, dass Int.init(exactly:) nil zurückgibt, wenn der angegebene Gleitkommawert nicht exakt als Ganzzahl darstellbar ist.
Der Initialisierer beantwortet eine eng umrissene Frage: Passt dieser angegebene Wert exakt? Er stellt weder fest, ob die ursprüngliche Geometrie gültig war, noch ob die gewählte Rundungsregel zum Layout passt oder ob es praktikabel ist, so viele Kacheln zu erstellen. Das sind separate Prüfungen in der folgenden Implementierung.
Ein vollständiges Swift-Programm für eine begrenzte Kachelanzahl
Das folgende Programm verwendet Double für Koordinaten und Dimensionen und Int für Anzahlen. Es verlangt endliche, nichtnegative Ausdehnungen, endliche, positive Kacheldimensionen und eine positive, anwendungsdefinierte Kachelgrenze. Hat eine Zeichenfläche in einer der beiden Dimensionen die Ausdehnung null, entsteht ein leeres Raster. Umgekehrte Grenzen werden zurückgewiesen, statt stillschweigend neu angeordnet zu werden.
Speichere dieses vollständige Programm als GridCount.swift und führe es mit swift GridCount.swift auf einem Mac mit installierter Swift-Toolchain aus. Es verwendet ausschließlich die Swift-Standardbibliothek und benötigt daher keine Imports. Das Programm wird mit den erwarteten Ergebnissen präsentiert; es wurde geprüft, aber im Rahmen dieser Recherche nicht mit einem Swift-Compiler ausgeführt.
enum GridError: Error {
case invalidInput
case unrepresentableAxis
case countOverflow
case exceedsLimit
}
struct GridCount {
let columns: Int
let rows: Int
let total: Int
}
func coveringAxis(extent: Double, tile: Double) throws -> Int {
guard extent.isFinite, extent >= 0,
tile.isFinite, tile > 0 else {
throw GridError.invalidInput
}
if extent == 0 { return 0 }
let quotient = extent / tile
guard quotient.isFinite, quotient > 0,
let count = Int(exactly: quotient.rounded(.up)),
count > 0 else {
throw GridError.unrepresentableAxis
}
return count
}
func coveringGrid(
width: Double,
height: Double,
tileWidth: Double,
tileHeight: Double,
maxCells: Int
) throws -> GridCount {
guard width.isFinite, width >= 0,
height.isFinite, height >= 0,
tileWidth.isFinite, tileWidth > 0,
tileHeight.isFinite, tileHeight > 0,
maxCells > 0 else {
throw GridError.invalidInput
}
if width == 0 || height == 0 {
return GridCount(columns: 0, rows: 0, total: 0)
}
let columns = try coveringAxis(extent: width, tile: tileWidth)
let rows = try coveringAxis(extent: height, tile: tileHeight)
let product = columns.multipliedReportingOverflow(by: rows)
guard !product.overflow else {
throw GridError.countOverflow
}
guard product.partialValue <= maxCells else {
throw GridError.exceedsLimit
}
return GridCount(
columns: columns,
rows: rows,
total: product.partialValue
)
}
let left: Double = 20
let right: Double = 670
let top: Double = 30
let bottom: Double = 440
do {
let grid = try coveringGrid(
width: right - left,
height: bottom - top,
tileWidth: 64,
tileHeight: 64,
maxCells: 10_000
)
print("columns=\(grid.columns), rows=\(grid.rows), total=\(grid.total)")
// Expected: columns=11, rows=7, total=77
} catch {
print("Grid calculation failed: \(error)")
}
Was die Prüfungen bewirken
Die Prüfungen auf endliche Werte weisen NaN und Unendlich zurück, bevor einer der beiden Werte in eine Anzahl umgewandelt werden kann. Apple gibt an, dass isFinite für beide Werte false ist. Positive Kacheldimensionen schließen außerdem eine Division durch null und negative Kachelgrößen aus.
Die getrennte Prüfung des Quotienten ist auch nach der Validierung der Eingaben wichtig. Die Implementierung weist einen nicht endlichen Quotienten zurück und weist auch den Wert null zurück, wenn die Ausdehnung positiv war. Letztere Prüfung macht einen extrem kleinen positiven Quotienten, der zu null unterläuft, zu einem ausdrücklichen Fehler statt zu einer leeren Achse. Das ist eine bewusste Grenze dieses Beispiels.
Nach jeder erfolgreichen Achsenumwandlung muss noch das Gesamtergebnis geprüft werden. Zwei gültige Int-Werte können ein Produkt ergeben, das außerhalb des Wertebereichs des Typs liegt. Laut Apples Dokumentation zu multipliedReportingOverflow(by:) enthält partialValue das vollständige Produkt nur dann, wenn overflow false ist. Das Programm prüft dieses Flag, bevor es das Produkt verwendet.
Der abschließende Vergleich mit maxCells dient einem anderen Zweck: Er setzt eine Anwendungsgrenze durch, selbst wenn die Berechnung im Wertebereich bleibt. Der beispielhafte Wert 10.000 ist eine vom Aufrufer gewählte Vorgabe und keine Empfehlung für Speicher- oder Zeichenleistung. Wähle eine Grenze anhand dessen, was deine Anwendung tatsächlich für jede Kachel erstellt.
Einen Gleitkommavergleich an der Ganzzahlgrenze vermeiden
Eine naheliegende Prüfung ist value <= Double(Int.max), gefolgt von Int(value). Dieser Vergleich kann irreführend sein, weil die Umwandlung der Ganzzahlgrenze in eine Gleitkommazahl zu einer Rundung führen kann. Apple beschreibt Double.init(_:) aus Int ausdrücklich als Umwandlung in die nächstmögliche Darstellung.
Auf einem 64-Bit-Zielsystem ist Int.max gleich 9_223_372_036_854_775_807. Bei der Umwandlung in Double wird auf 9_223_372_036_854_775_808 gerundet, also um eins nach oben. Ein Gleitkommawert, der dieser gerundeten Grenze entspricht, würde den Vergleich bestehen, läge aber weiterhin außerhalb des Ganzzahlbereichs. Apple beschreibt den Zusammenhang zwischen Plattformbreite und Ganzzahlgröße auf der Referenzseite zu Int.
Dieses Grenzbeispiel ist eine Rechenillustration und keine vorgeschlagene Größe für eine Zeichenfläche. Es erklärt, warum das Programm Int(exactly:) direkt die Prüfung der Darstellbarkeit überlässt. So wird auch vermieden, eine maximale Anzahl fest einzutragen, die an eine bestimmte Ganzzahlbreite gebunden ist.
Festlegen, wie Werte nahe einer Kachelgrenze behandelt werden
Aufrunden folgt dem tatsächlich gespeicherten Wert. Ein Quotient knapp über zwei erfordert bei dieser Abdeckungsregel drei Kacheln. Swifts Eigenschaft nextUp liefert den nächstgrößeren darstellbaren Gleitkommawert, daher ist (2.0).nextUp.rounded(.up) gleich 3.0. Die Bedeutung dieses benachbarten Werts ist in Apples Referenz zu nextUp dokumentiert.
Ziehe nicht einfach ein beliebiges Epsilon ab, nur damit ein angezeigter Wert ganzzahlig aussieht. Dadurch ändert sich die Abdeckungsregel, und eine Kachel, die für einen kleinen tatsächlichen Rest benötigt wird, kann entfallen. Wenn die Anwendung Koordinaten gezielt auf ein Raster einrasten lässt, lege diese Einrastregel vorgelagert fest und berechne die Anzahlen anhand der eingerasteten Geometrie. Wenn Dimensionen aus exakten ganzzahligen Einheiten stammen, solltest du erwägen, diese Einheiten während der Anzahlberechnung beizubehalten.
Das Beispiel setzt außerdem voraus, dass die Subtraktion der angegebenen Koordinaten brauchbare Ausdehnungen ergibt. Eine exakte abschließende Umwandlung kann keine Details wiederherstellen, die bei früheren Koordinatenberechnungen verloren gegangen sind. Halte die Koordinatenbeträge für das Layout angemessen und verwende für Zeichenfläche und Kacheldimensionen dieselben Einheiten.
Fälle prüfen, an denen sich korrektes Verhalten erkennen lässt
Verwende Grenzfälle, um deine Anpassung zu überprüfen, insbesondere wenn du die Rundungsregel oder das Verhalten bei leeren Rastern änderst. Die folgenden erwarteten Ergebnisse entsprechen dem beschriebenen Vertrag des Programms; sie sind keine berichteten Swift-Testergebnisse.
| Synthetische Eingabe | Erwartetes Ergebnis |
|---|---|
Zeichenfläche 650 × 410, Kacheln 64 × 64, Grenze 10_000 |
11 Spalten, 7 Zeilen, 77 Kacheln |
Zeichenfläche 128 × 128, Kacheln 64 × 64, Grenze 10_000 |
2 Spalten, 2 Zeilen, 4 Kacheln |
Zeichenfläche 129 × 128, Kacheln 64 × 64, Grenze 10_000 |
3 Spalten, 2 Zeilen, 6 Kacheln |
| Eine der beiden Ausdehnungen der Zeichenfläche ist null; andere Eingaben sind gültig | Leeres Raster mit insgesamt null Kacheln |
| Negative Ausdehnung, Kacheldimension null oder nicht endliche Eingabe | invalidInput |
| Eine positive Ausdehnung geteilt durch eine Kachel ergibt null oder Unendlich | unrepresentableAxis |
Eine aufgerundete Achse passt nicht in Int |
unrepresentableAxis |
| Gültige Achsenanzahlen, deren Produkt überläuft | countOverflow |
Das Beispiel mit 77 Kacheln und einer Grenze von 50 |
exceedsLimit |
Für diese Mac-Zeichenfläche solltest du die Geometrie in Gleitkommawerten belassen, bis jede Achse berechnet wurde, aufrunden, um teilweise Kacheln am Rand einzubeziehen, und über Int(exactly:) umwandeln. Prüfe anschließend die Multiplikation und die Anwendungsgrenze, bevor du das Raster erstellst. Mit dieser Reihenfolge werden aus den berechneten Dimensionen Anzahlen, und Fehlerfälle lassen sich ausdrücklich überprüfen.
Quellen
- FloatingPointRoundingRule — Apple
- rounded(_:) — Apple
- BinaryInteger.init(_:) — Apple
- Int.init(exactly:) — Apple
- isFinite — Apple
- multipliedReportingOverflow(by:) — Apple
- Double.init(_:) — Apple
- Int — Apple
- nextUp — Apple