要在 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:) 进行转换。然后在创建网格之前验证乘法和应用程序限制。该序列将计算出的尺寸转换为具有显式、可审查失败行为的计数。
来源
- FloatingPointRoundingRule — Apple
- rounded(_:) — Apple
- BinaryInteger.init(_:) — Apple
- Int.init(exactly:) — Apple
- isFinite — Apple
- multipliedReportingOverflow(by:) — Apple
- Double.init(_:) — Apple
- Int — Apple
- nextUp — Apple