Swiftで計算したグリッドサイズを整数の個数に変換するには、まず丸め規則を選び、次にInt(exactly:)を使って整数で正確に表現できるか確認します。タイルで覆うMacのキャンバスでは、各軸を切り上げ、最後の乗算でオーバーフローが起きないか確認します。

この記事では、デスクトップ描画ツール向けにこの計算を説明します。長方形のキャンバスと固定サイズのタイルが与えられたとき、全体を覆うのに必要な列数、行数、タイル総数を求めます。以下の座標と個数はすべて説明用の値です。グリッドはキャンバス自体の原点から始まり、隙間はなく、外周には部分的なタイルを許容します。

グリッドの用途に応じて丸め規則を選ぶ

小数のグリッド寸法を扱うには、型変換より先に方針を決める必要があります。幅が650ポイントのキャンバスに、幅64ポイントのタイルを並べると、計算上の寸法は650.0 / 64.0、つまり10.15625です。幅全体を覆うには11列必要です。この幅に収まる完全なタイルだけを数える場合は10列です。

Swiftには、こうした用途に応じた明示的な丸め規則が用意されています。.upは正の無限大方向に、.downは負の無限大方向に丸め、.towardZeroは小数部分を切り捨てます。最も近い値への丸めには、ちょうど中間の値に対する個別の規則があります。これらの動作はAppleのFloatingPointRoundingRuleのドキュメントで定義されています。

読者の目的 0以上の軸寸法に使う規則 10.15625の結果
端のタイルも含めて全体を覆う .up 11.0
寸法内に収まる完全なタイルを数える .down 10.0
最も近い整数の寸法を選ぶ .toNearestOrAwayFromZero 10.0
計算した寸法がすでに整数であることを要求する 丸めずに、直接Int(exactly:)を試す nil

全体を覆う用途では.rounded(.up)を使います。規則を指定せずに.rounded()を呼び出すと、中間値を0から遠ざける方向に丸める最近接丸めが選ばれ、このキャンバスの一部が覆われないことがあります。また、Doubleを丸めても結果は整数値を持つDoubleであり、整数への変換は別の手順です。Appleのrounded(_:)のドキュメントを参照してください。

列数と行数を個別に丸める

説明用のキャンバスが、水平方向には座標20から670まで、垂直方向には座標30から440まで広がっているとします。幅は650ポイント、高さは410ポイントです。一辺64ポイントの正方形タイルを使う場合、全体を覆う計算は次のとおりです。

  • 列数:ceil(650 / 64) = 11。
  • 行数:ceil(410 / 64) = 7。
  • 合計:11 × 7 = 77枚のタイル。

これらは、示した配置から導かれる計算例です。キャンバス面積をタイル面積で割って最後に一度だけ丸めると66になりますが、必要な長方形の配置を表していません。各行に11個のタイル位置が必要で、キャンバスにはそのような行が7つ必要です。両方の寸法を丸め終えるまで、この2つの値を個別に保持してください。

この式は、最初のタイルがキャンバスの原点から始まることを前提としています。タイルのグリッドが別の位置を基準にしている場合、開始位置のずれによって、長方形が交差するセルの数が変わることがあります。幅だけの計算を適用する前に、その位置合わせを確定してください。

整数への直接変換だけでは不十分な理由

Swiftの標準的な浮動小数点数から整数へのイニシャライザは、0方向に丸めます。そのため、Int(10.15625)は10になります。また、切り捨て後の値が変換先の整数型に収まらなければなりません。Appleのドキュメントによると、範囲外の変換は実行時エラーを起こすことがあります。BinaryInteger.init(_:)を参照してください。

したがって、計算した全体を覆う寸法には、まずInt(exactly: value.rounded(.up))を適用し、必要に応じてオプショナル値を処理するのが有効です。AppleのInt.init(exactly:)のドキュメントによると、渡された浮動小数点値を整数として正確に表現できない場合、nilが返されます。

このイニシャライザが答えるのは、渡した値が正確に収まるかどうかという限定的な問いです。元のジオメトリが有効か、選んだ丸め規則がレイアウトに適しているか、その数のタイルを実際に作成できるかまでは確認しません。以下の実装では、それぞれ別に確認します。

タイル数に上限を設けるSwiftプログラム

次のプログラムでは、座標と寸法にDouble、個数にIntを使います。有限かつ0以上の範囲、有限かつ正のタイル寸法、そしてアプリケーション側で定めた正のタイル上限を要求します。キャンバスのどちらかの範囲が0の場合、空のグリッドを返します。範囲の前後が逆の場合は、暗黙に並べ替えずに拒否します。

このプログラム全体をGridCount.swiftとして保存し、Swiftツールチェーンを使えるMacでswift GridCount.swiftを実行してください。Swift標準ライブラリのみを使うため、インポートは不要です。以下には期待される結果を示します。コードは確認しましたが、この調査中に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)")
}

各チェックで確認できること

有限値のチェックにより、NaNや無限大が個数に変換される前に拒否できます。Appleの仕様では、isFiniteはどちらの場合もfalseです。タイル寸法を正の値に限定することで、ゼロ除算や負のタイル寸法も除外できます。

入力を検証した後でも、商を個別に確認することが重要です。この実装は、商が有限でない場合を拒否し、範囲が正なのに商が0の場合も拒否します。後者のチェックにより、極めて小さい正の商がアンダーフローで0になったとき、軸が空になるのではなく明示的な失敗として扱われます。これは、この例で意図して設けた境界条件です。

各軸の変換に成功しても、合計の確認は必要です。有効なInt値同士の積でも、その型の範囲を超えることがあります。AppleのmultipliedReportingOverflow(by:)のドキュメントによると、partialValueに積全体が入るのは、overflowがfalseの場合だけです。プログラムは積を使う前にこのフラグを確認します。

最後のmaxCellsとの比較は別の目的を持ち、計算上は問題がなくてもアプリケーションの上限を適用します。例として示す10,000は呼び出し側が選ぶ方針であり、メモリ使用量や描画性能に関する推奨値ではありません。各タイルに対してアプリケーションが実際に作成するものを踏まえて、上限を決めてください。

整数の境界で浮動小数点数の範囲チェックを避ける

一見よさそうな方法は、value <= Double(Int.max)を確認してからInt(value)を実行することです。この比較は誤解を招くことがあります。整数の上限を浮動小数点数に変換すると、値が丸められる可能性があるためです。Appleは、IntからのDouble.init(_:)を、可能な限り近い値への変換と明記しています。

64ビットのターゲットでは、Int.maxは9_223_372_036_854_775_807です。これをDoubleに変換すると9_223_372_036_854_775_808に丸められ、元の値より1大きくなります。この丸められた上限と等しい浮動小数点値は、整数の範囲外であるにもかかわらず比較を通過します。プラットフォームのビット幅と整数サイズの関係については、AppleのIntリファレンスページを参照してください。

この境界の例は算術上の説明であり、キャンバスサイズの提案ではありません。プログラムが表現可能かどうかの確認をInt(exactly:)に直接委ねる理由を示しています。また、特定の整数幅に依存する最大個数をハードコードせずに済みます。

タイル境界付近の値の扱いを決める

切り上げは、実際に保持されている値に従います。この全体を覆う方針では、商が2をわずかに上回る場合、タイルは3枚必要です。SwiftのnextUpプロパティは、表現可能な浮動小数点数のうち次に大きい値を返すため、(2.0).nextUp.rounded(.up)は3.0になります。この隣接する値の意味については、AppleのnextUpリファレンスに説明があります。

表示値が整数に見えるようにするだけのために、任意のイプシロンを引かないでください。そうすると全体を覆う方針が変わり、実際に存在する小さな余りに必要なタイルを取り除くことがあります。アプリケーションで座標を意図的にグリッドに合わせる場合は、その規則を上流で定義し、位置合わせ後のジオメトリから個数を計算してください。寸法が正確な整数単位で得られる場合は、個数の計算が終わるまでその単位を保持することも検討してください。

この例では、渡された座標の差から有用な範囲を計算できることも前提としています。最後の変換が正確でも、それ以前の座標計算ですでに失われた詳細を取り戻すことはできません。レイアウトに適した座標の大きさを保ち、キャンバスとタイル寸法に同じ単位を使ってください。

正しい動作を見分けられるケースを確認する

特に丸め方針や空のグリッドの扱いを変更する場合は、境界値を使って実装を確認してください。次の期待結果は、このプログラムで示した仕様に基づくものであり、Swiftによるテスト実行結果ではありません。

説明用の入力 期待される結果
キャンバス650 × 410、タイル64 × 64、上限10_000 11列、7行、77枚
キャンバス128 × 128、タイル64 × 64、上限10_000 2列、2行、4枚
キャンバス129 × 128、タイル64 × 64、上限10_000 3列、2行、6枚
キャンバスのどちらかの範囲が0で、ほかの入力は有効 タイル総数0の空グリッド
負の範囲、0のタイル寸法、または有限でない入力 invalidInput
正の範囲をタイル寸法で割ると0または無限大になる unrepresentableAxis
切り上げ後の軸の値がIntに収まらない unrepresentableAxis
有効な軸の個数同士の積がオーバーフローする countOverflow
77枚の例で上限が50 exceedsLimit

このMacキャンバスの例では、各軸の計算が終わるまでジオメトリを浮動小数点数で保持し、端にかかる部分タイルも含めるために切り上げ、Int(exactly:)を通して変換します。その後、グリッドを作成する前に乗算結果とアプリケーション上限を検証します。この手順なら、計算した寸法を明示的で確認可能な失敗動作を備えた個数に変換できます。

参考資料