Чтобы преобразовать вычисленный размер сетки в целое число, сначала выберите правило округления, затем используйте 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 GridCount.swift на Mac с установленной средой 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:), partialValue содержит полное произведение только тогда, когда overflow имеет значение false. Программа проверяет этот флаг до использования произведения.
Итоговое сравнение maxCells решает другую задачу: оно обеспечивает соблюдение ограничения приложения, даже если арифметическое значение укладывается в диапазон. Иллюстративное значение 10 000 — это политика, выбранная вызывающим кодом, а не рекомендация по расходу памяти или производительности рисования. Выбирайте предел с учётом того, что именно приложение создаёт для каждой плитки.
Не проверяйте границу целого числа через числа с плавающей точкой
Может показаться удобным проверить value <= Double(Int.max), а затем Int(value). Такое сравнение может ввести в заблуждение, поскольку преобразование целочисленной границы в число с плавающей точкой может привести к округлению. Apple прямо указывает, что Double.init(_:) для Int выполняет преобразование к ближайшему возможному представлению.
На 64-разрядной платформе значение Int.max равно 9_223_372_036_854_775_807. При преобразовании в Double оно округляется до 9_223_372_036_854_775_808, то есть на единицу больше. Значение с плавающей точкой, равное этой округлённой границе, пройдёт сравнение, хотя останется за пределами диапазона целых чисел. Связь разрядности платформы с размером целого числа описана на справочной странице Apple Int.
Этот пример с границей иллюстрирует арифметику, а не предлагает использовать такой размер холста. Он объясняет, почему программа поручает Int(exactly:) напрямую проверить возможность представления значения. Это также позволяет не задавать максимальное количество, привязанное к разрядности конкретного целочисленного типа.
Решите, как обрабатывать значения вблизи границы плитки
Округление вверх зависит от фактически сохранённого значения. Если частное чуть больше двух, согласно этой политике покрытия нужны три плитки. Свойство nextUp в Swift возвращает следующее большее представимое значение с плавающей точкой, поэтому (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 плиток |
| Один из размеров холста равен нулю; остальные входные данные корректны | Пустая сетка, всего ноль плиток |
| Отрицательный размер, нулевой размер плитки или не конечное входное значение | invalidInput |
| Положительный размер, разделённый на размер плитки, даёт ноль или бесконечность | unrepresentableAxis |
Округлённый вверх размер оси не помещается в Int |
unrepresentableAxis |
| Корректные количества по осям, произведение которых вызывает переполнение | countOverflow |
Пример с 77 плитками и пределом 50 |
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