要在 Swift 中将计算得出的网格尺寸转换为整数计数,首先选择舍入规则,然后使用 Int(exactly:) 来检查可表示性。对于由平铺覆盖的 Mac 画布,将每个轴向上舍入并检查最终乘法是否溢出。

本文针对桌面绘图工具开发了该计算:给定一个矩形画布和固定大小的平铺,确定覆盖它所需的列数、行数和总平铺数。下文中的所有坐标和计数均为合成数据。网格从画布自身的原点开始,不使用间隙,并允许外边缘出现部分平铺。

根据网格用途选择舍入规则

小数网格尺寸在需要类型转换之前先需要一个策略。如果画布宽 650 点,每个平铺宽 64 点,则计算得出的尺寸为 650.0 / 64.0,即 10.15625。覆盖整个宽度需要 11 列。仅计算适合该宽度的完整平铺数得到 10。

Swift 为这些不同任务提供了显式的舍入规则。.up 向正无穷大舍入,.down 向负无穷大舍入,.towardZero 移除小数部分。最近舍入对平局有单独的规则。这些行为在 Apple 的 FloatingPointRoundingRule 文档 中定义。

读者任务 非负轴尺寸的规则 10.15625 的结果
覆盖全长,包括边缘平铺 .up 11.0
计算适合范围内的完整平铺数 .down 10.0
选择最近的整数尺寸 .toNearestOrAwayFromZero 10.0
要求计算出的尺寸已经是整数 不舍入;直接尝试 Int(exactly:) nil

对于覆盖任务,使用 .rounded(.up)。不带规则调用 .rounded() 会选择最近舍入(平局时远离零),这可能导致部分画布未被覆盖。此外,对 Double 进行舍入会返回一个整数值的 Double;整数转换仍是一个单独的步骤。请参阅 Apple 的 rounded(_:) 文档。

分别对列和行进行舍入

假设合成画布从水平坐标 20 延伸到 670,垂直坐标 30 延伸到 440。其宽度为 650 点,高度为 410 点。使用边长为 64 点的正方形平铺,覆盖计算如下:

  • 列数:ceil(650 / 64) = 11。
  • 行数:ceil(410 / 64) = 7。
  • 总计:11 × 7 = 77 个平铺。

这些是说明性的算术结果,源自所述布局。将画布面积除以平铺面积并舍入一次得到 66,这并不描述所需的矩形排列。每行需要 11 个平铺位置,画布需要七行这样的平铺。保留这两个维度,直到两者都已舍入。

此公式假设第一个平铺从画布原点开始。如果平铺属于锚定在别处的网格,则起始偏移量会改变矩形相交的单元格数量。在应用仅宽度计算之前,请确定该对齐方式。

为何直接整数转换不足够

Swift 的普通浮点数到整数初始化器会向零舍入。因此,Int(10.15625) 产生 10。源值在截断后也必须适合目标整数类型;Apple 文档指出,超出范围的转换可能导致运行时错误。请参阅 BinaryInteger.init(_:)。

对于计算得出的覆盖尺寸,因此有用的序列是 Int(exactly: value.rounded(.up)),后跟可选处理。Apple 文档指出,当提供的浮点值无法精确表示为整数时,Int.init(exactly:) 返回 nil。

该初始化器回答一个狭窄的问题:提供的值是否精确匹配?它不确定原始几何是否有效、所选舍入规则是否适合布局,或创建那么多平铺是否切合实际。这些是在下面实现中的单独检查。

一个完整的有界平铺计数 Swift 程序

以下程序对坐标和尺寸使用 Double,对计数使用 Int。它要求有限、非负的范围;有限、正数的平铺尺寸;以及一个正的应用程序定义的平铺限制。任一范围等于零的画布将产生空网格。反转的边界会被拒绝,而不是静默重新排列。

将此完整程序保存为 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。要求正数的平铺尺寸也排除了除以零和负数平铺尺寸。

即使在验证输入后,单独检查商也很重要。实现会拒绝非有限商,并在范围为正时拒绝零。后一种检查使得极小的正商下溢为零成为显式失败,而不是空轴。这是示例的故意边界。

每次成功的轴转换仍留下总数待检查。两个有效的 Int 值的乘积可能超出类型范围。Apple 的 multipliedReportingOverflow(by:) 文档 指定仅当 overflow 为 false 时,partialValue 才包含完整乘积。程序在使用乘积前检查该标志。

最后的 maxCells 比较服务于不同目的:即使算术运算符合,它也会强制执行应用程序限制。说明性的 10,000 值是调用者选择的策略,而非关于内存或绘制性能的建议。根据您的应用程序实际为每个平铺创建的内容来选择限制。

避免在整数边界进行浮点数范围检查

一个诱人的检查是 value <= Double(Int.max) 后跟 Int(value)。该比较可能具有误导性,因为将整数边界转换为浮点数可能会对其进行舍入。Apple 明确将 Double.init(_:) from Int 描述为转换为最接近的可能表示。

在 64 位目标上,Int.max 为 9_223_372_036_854_775_807。其 Double 转换舍入为 9_223_372_036_854_775_808,大 1。等于该舍入边界的浮点值会通过比较,但仍位于整数范围之外。Apple 在其 Int 参考页面 上记录了平台宽度与整数大小之间的关系。

此边界示例是算术说明,而非提议的画布尺寸。它解释了为什么程序要求 Int(exactly:) 直接执行可表示性检查。它还避免了将最大计数硬编码为特定的整数宽度。

决定如何处理接近平铺边界的值

向上舍入遵循实际存储的值。略大于 2 的商在此覆盖策略下需要三个平铺。Swift 的 nextUp 属性提供下一个更大的可表示浮点值,因此 (2.0).nextUp.rounded(.up) 为 3.0。该相邻值的含义记录在 Apple 的 nextUp 参考 中。

不要仅为了让显示的值看起来像整数就减去任意 epsilon。这会改变覆盖策略,并可能移除小的真实余数所需的平铺。如果应用程序有意将坐标吸附到网格,请在上游定义该吸附规则,并从吸附后的几何形状计算计数。如果尺寸源自精确的整数单位,请考虑在计数计算中保留这些单位。

该示例还假设减去提供的坐标可得出有用的范围。精确的最终转换无法恢复早期坐标算术中已丢失的细节。保持坐标量级适合布局,并对画布和平铺尺寸应用相同的单位。

检查区分正确行为的用例

使用边界用例验证您的改编,特别是如果您更改舍入策略或空网格行为。以下预期结果源自该程序的既定约定;它们不是报告的 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 个平铺
任一画布范围为零;其他输入有效 总计为零的空网格
负范围、零平铺尺寸或非有限输入 invalidInput
正范围除以平铺产生零或无穷大 unrepresentableAxis
向上舍入的轴无法放入 Int unrepresentableAxis
乘积溢出的有效轴计数 countOverflow
限制为 50 的 77 平铺示例 exceedsLimit

对于此 Mac 画布任务,在每个轴计算完成前保持几何为浮点数,向上舍入以包含部分边缘平铺,并通过 Int(exactly:) 进行转换。然后在创建网格之前验证乘法和应用程序限制。该序列将计算出的尺寸转换为具有显式、可审查失败行为的计数。

来源