Aby zamienić obliczony rozmiar siatki na liczbę całkowitą w Swift, najpierw wybierz regułę zaokrąglania, a następnie użyj Int(exactly:), aby sprawdzić, czy wartość jest reprezentowalna. W przypadku płótna Mac pokrywanego kafelkami zaokrąglij każdą oś w górę i sprawdź, czy końcowe mnożenie nie powoduje przepełnienia.

W tym artykule omówiono te obliczenia na przykładzie komputerowego narzędzia do rysowania: mając prostokątne płótno i kafelki o stałym rozmiarze, ustalamy liczbę kolumn, wierszy i łączną liczbę kafelków potrzebnych do jego pokrycia. Wszystkie podane niżej współrzędne i liczby są przykładowe. Siatka zaczyna się w początku układu współrzędnych samego płótna, nie ma odstępów i dopuszcza częściowe kafelki przy zewnętrznych krawędziach.

Wybierz regułę zaokrąglania zależnie od celu siatki

Ułamkowy wymiar siatki wymaga ustalenia reguły, zanim zostanie przekonwertowany na inny typ. Jeśli płótno ma 650 punktów szerokości, a każdy kafelek ma 64 punkty szerokości, obliczony wymiar wynosi 650.0 / 64.0, czyli 10.15625. Do pokrycia całej szerokości potrzeba 11 kolumn. Jeśli liczymy tylko pełne kafelki mieszczące się na tej szerokości, otrzymamy 10.

Swift udostępnia jawne reguły zaokrąglania do tych różnych zadań. .up zaokrągla w kierunku dodatniej nieskończoności, .down w kierunku ujemnej nieskończoności, a .towardZero usuwa część ułamkową. Zaokrąglanie do najbliższej wartości ma osobne reguły rozstrzygania remisów. Te zachowania opisano w dokumentacji Apple FloatingPointRoundingRule.

Zadanie Reguła dla nieujemnego wymiaru osi Wynik dla 10.15625
Pokrycie całego obszaru, łącznie z kafelkiem na krawędzi .up 11.0
Zliczenie pełnych kafelków mieszczących się w obszarze .down 10.0
Wybór najbliższego całkowitego wymiaru .toNearestOrAwayFromZero 10.0
Wymaganie, by obliczony wymiar był już całkowity Bez zaokrąglania; spróbuj bezpośrednio użyć Int(exactly:) nil

W przypadku pokrycia obszaru użyj .rounded(.up). Wywołanie .rounded() bez reguły wybiera zaokrąglanie do najbliższej wartości, a remisy rozstrzyga z dala od zera, co może pozostawić część tego płótna niepokrytą. Ponadto zaokrąglenie wartości Double zwraca wartość Double bez części ułamkowej; konwersja na liczbę całkowitą to osobny krok. Zobacz dokumentację Apple rounded(_:).

Zaokrąglaj osobno liczbę kolumn i wierszy

Załóżmy, że przykładowe płótno rozciąga się od współrzędnej poziomej 20 do 670 oraz od współrzędnej pionowej 30 do 440. Ma 650 punktów szerokości i 410 punktów wysokości. Przy kwadratowych kafelkach o boku długości 64 punktów obliczenie pokrycia wygląda następująco:

  • Kolumny: ceil(650 / 64) = 11.
  • Wiersze: ceil(410 / 64) = 7.
  • Łącznie: 11 × 7 = 77 kafelków.

To przykładowe wyniki arytmetyczne wynikające z podanego układu. Dzielenie powierzchni płótna przez powierzchnię kafelka i jednokrotne zaokrąglenie daje 66, co nie opisuje wymaganej prostokątnej siatki. Każdy wiersz wymaga 11 miejsc na kafelki, a płótno potrzebuje siedmiu takich wierszy. Zachowaj oba wymiary do czasu ich zaokrąglenia.

Ten wzór zakłada, że pierwszy kafelek zaczyna się w początku układu współrzędnych płótna. Jeśli kafelki należą do siatki zakotwiczonej w innym miejscu, przesunięcie początkowe może zmienić liczbę komórek przecinanych przez prostokąt. Przed zastosowaniem obliczeń opartych wyłącznie na szerokości ustal wyrównanie siatki.

Dlaczego bezpośrednia konwersja na liczbę całkowitą nie wystarcza

Standardowy inicjalizator Swift konwertujący liczbę zmiennoprzecinkową na całkowitą zaokrągla w kierunku zera. Dlatego Int(10.15625) daje 10. Wartość źródłowa po obcięciu musi też mieścić się w zakresie typu docelowego; Apple dokumentuje, że konwersja wartości spoza zakresu może spowodować błąd w czasie wykonywania. Zobacz BinaryInteger.init(_:).

W przypadku obliczonego wymiaru potrzebnego do pokrycia obszaru właściwa kolejność to Int(exactly: value.rounded(.up)), a następnie opcjonalna obsługa wyniku. Apple dokumentuje, że Int.init(exactly:) zwraca nil, jeśli podanej wartości zmiennoprzecinkowej nie da się dokładnie przedstawić jako liczby całkowitej.

Ten inicjalizator odpowiada na wąskie pytanie: czy podana wartość mieści się w typie bez utraty precyzji? Nie sprawdza, czy pierwotna geometria była prawidłowa, czy wybrana reguła zaokrąglania pasuje do układu ani czy utworzenie takiej liczby kafelków jest praktyczne. Poniższa implementacja sprawdza te kwestie osobno.

Kompletny program Swift z limitem liczby kafelków

Poniższy program używa Double dla współrzędnych i wymiarów oraz Int dla liczników. Wymaga skończonych, nieujemnych wymiarów obszaru, skończonych, dodatnich wymiarów kafelka i dodatniego limitu liczby kafelków określonego przez aplikację. Jeśli którykolwiek wymiar płótna wynosi zero, wynikiem jest pusta siatka. Odrzucone zostają granice podane w odwrotnej kolejności, zamiast ich cichego przestawiania.

Zapisz ten kompletny program w pliku GridCount.swift i uruchom poleceniem swift GridCount.swift na Macu z dostępnym łańcuchem narzędzi Swift. Program korzysta wyłącznie ze standardowej biblioteki Swift, więc nie wymaga importów. Przedstawiono oczekiwane wyniki; program został sprawdzony, ale podczas tych badań nie uruchomiono go przy użyciu kompilatora Swift.

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)")
}

Co zapewniają te kontrole

Kontrole wartości skończonych odrzucają NaN i nieskończoność, zanim którakolwiek z nich zostanie zamieniona na liczbę. Apple określa, że isFinite zwraca false dla obu tych wartości. Wymóg dodatnich wymiarów kafelka wyklucza także dzielenie przez zero i ujemne rozmiary kafelków.

Osobne sprawdzenie ilorazu jest istotne nawet po zweryfikowaniu danych wejściowych. Implementacja odrzuca niefinitywny iloraz, a także zero, jeśli wymiar obszaru był dodatni. Ten drugi warunek jawnie obsługuje sytuację, gdy bardzo mały dodatni iloraz ulega zaokrągleniu do zera, zamiast potraktować taką oś jako pustą. To celowe ograniczenie przykładu.

Po pomyślnej konwersji wymiarów osi nadal trzeba sprawdzić ich iloczyn. Iloczyn dwóch prawidłowych wartości Int może wykraczać poza zakres tego typu. Dokumentacja Apple dotycząca multipliedReportingOverflow(by:) określa, że partialValue zawiera pełny iloczyn tylko wtedy, gdy overflow ma wartość false. Program sprawdza tę flagę przed użyciem iloczynu.

Końcowe porównanie maxCells służy innemu celowi: egzekwuje limit aplikacji nawet wtedy, gdy obliczenie mieści się w zakresie. Przykładowa wartość 10 000 to limit wybrany przez wywołującego, a nie zalecenie dotyczące pamięci lub wydajności rysowania. Ustal limit na podstawie tego, co aplikacja faktycznie tworzy dla każdego kafelka.

Unikaj porównywania granic typu całkowitego z wartościami zmiennoprzecinkowymi

Kuszącym rozwiązaniem jest sprawdzenie value <= Double(Int.max), a następnie Int(value). Takie porównanie może wprowadzać w błąd, ponieważ konwersja granicy typu całkowitego na zmiennoprzecinkowy może ją zaokrąglić. Apple wyraźnie opisuje Double.init(_:) z Int jako konwersję do najbliższej możliwej reprezentacji.

Na platformie 64-bitowej Int.max wynosi 9_223_372_036_854_775_807. Konwersja tej wartości na Double zaokrągla ją do 9_223_372_036_854_775_808, czyli o jeden więcej. Wartość zmiennoprzecinkowa równa tej zaokrąglonej granicy przejdzie porównanie, mimo że nadal wykracza poza zakres typu całkowitego. Apple opisuje zależność między szerokością platformy a rozmiarem liczby całkowitej na stronie dokumentacji Int.

Ten przykład graniczny ilustruje działanie arytmetyki i nie proponuje rozmiaru płótna. Wyjaśnia, dlaczego program zleca Int(exactly:) bezpośrednie sprawdzenie reprezentowalności. Pozwala też uniknąć wpisywania na stałe maksymalnej liczby zależnej od szerokości danego typu całkowitego.

Zdecyduj, jak obsługiwać wartości bliskie granicy kafelka

Zaokrąglanie w górę działa na podstawie faktycznie zapisanej wartości. Iloraz nieznacznie większy od dwóch wymaga trzech kafelków przy przyjętej regule pokrycia. Właściwość nextUp typu Swift udostępnia następną większą reprezentowalną wartość zmiennoprzecinkową, dlatego (2.0).nextUp.rounded(.up) wynosi 3.0. Znaczenie tej sąsiedniej wartości opisano w dokumentacji Apple nextUp.

Nie odejmuj arbitralnie wybranej wartości epsilon tylko po to, by wyświetlana wartość wyglądała na całkowitą. Zmienia to regułę pokrycia i może usunąć kafelek potrzebny do pokrycia niewielkiej, rzeczywistej reszty. Jeśli aplikacja celowo dopasowuje współrzędne do siatki, określ tę regułę wcześniej i oblicz liczbę kafelków na podstawie dopasowanej geometrii. Jeśli wymiary pochodzą z dokładnych jednostek całkowitych, rozważ zachowanie tych jednostek przez całe obliczenie.

Przykład zakłada również, że odejmowanie podanych współrzędnych daje użyteczne wymiary obszaru. Dokładna konwersja końcowego wyniku nie odzyska szczegółów utraconych na wcześniejszym etapie działań na współrzędnych. Dobieraj wartości współrzędnych do skali układu i stosuj te same jednostki do wymiarów płótna oraz kafelków.

Sprawdź przypadki pozwalające odróżnić poprawne działanie

Użyj przypadków granicznych, aby zweryfikować swoje rozwiązanie, zwłaszcza jeśli zmienisz regułę zaokrąglania lub sposób obsługi pustej siatki. Poniższe oczekiwane wyniki wynikają z opisanych założeń programu; nie są rezultatami jego uruchomienia w testach Swift.

Przykładowe dane wejściowe Oczekiwany wynik
Płótno 650 × 410, kafelki 64 × 64, limit 10_000 11 kolumn, 7 wierszy, 77 kafelków
Płótno 128 × 128, kafelki 64 × 64, limit 10_000 2 kolumny, 2 wiersze, 4 kafelki
Płótno 129 × 128, kafelki 64 × 64, limit 10_000 3 kolumny, 2 wiersze, 6 kafelków
Jeden z wymiarów płótna wynosi zero; pozostałe dane są prawidłowe Pusta siatka, łączna liczba kafelków wynosi zero
Ujemny wymiar, zerowy wymiar kafelka lub wartość niefinitywna invalidInput
Dodatni wymiar podzielony przez rozmiar kafelka daje zero lub nieskończoność unrepresentableAxis
Zaokrąglony w górę wymiar osi nie mieści się w Int unrepresentableAxis
Prawidłowe liczby kafelków na osiach, których iloczyn powoduje przepełnienie countOverflow
Przykład z 77 kafelkami i limitem 50 exceedsLimit

W przypadku płótna Maca zachowaj wymiary w typie zmiennoprzecinkowym do momentu obliczenia każdej osi, zaokrąglij w górę, aby uwzględnić częściowe kafelki przy krawędziach, i wykonaj konwersję za pomocą Int(exactly:). Następnie sprawdź mnożenie i limit aplikacji przed utworzeniem siatki. Dzięki temu obliczone wymiary zostaną zamienione na liczby z jawną i możliwą do sprawdzenia obsługą błędów.

Źródła