Работая с большими списками данных, мы разбиваем их на страницы - так и пользователю удобнее, и не нагружает нашу систему. А значит вы наверняка решали задачу определения общего количества страниц. Давайте покажу вам один простой и элегантный способ.
Пагинация
В классическом виде задача звучит так: у нас есть всего TotalCount
записей в базе данных, нужно вывести их постранично по BatchSize
на страницу.
Нужно определить количество страниц - PageCount
. Для этого нужно поделить TotalCount
на BatchSize
и если результат получился дробным, то округлить его вверх.
| TotalCount | BatchSize | TotalCount/BatchSize | PageCount |
|------------|-----------|----------------------|----------:|
| 90 | 10 | 9 | 9 |
| 99 | 10 | 9.9 | 10 |
| 100 | 10 | 10 | 10 |
| 101 | 10 | 10.1 | 11 |
Math.Ceiling
В C# можно воспользоваться методом Math.Ceiling
- он округляет вещественное число до ближайшего целого, которое больше либо равно переданному аргументу.
var PageCount = (int)Math.Ceiling((double)TotalCount / BatchSize);
Мы получим корректное значение. Но как вы заметили, нам вначале нужно преобразовать наши целые числа TotalCount
, BatchSize
в вещественные, вызвать метод, а потом обратно преобразовать результат в целочисленный.
Остаток от деления
Можно заметить, если TotalCount
делится нацело на BatchSize
, то результатом является просто частное TotalCount/BatchSize
, иначе нужно добавить 1 к результату.
Преобразуем нашу формулу:
var PageCount = TotalCount / BatchSize + (TotalCount % BatchSize == 0 ? 0 : 1);
Мы избавились от преобразований типа и вызова метода, но добавился один тернарный оператор. Можно ли сделать еще проще? Оказывается, можно.
Целочисленное деление
Как мы знаем, целочисленное деление всегда даёт округление результата к меньшему либо равному значению. Давайте воспользуемся этой особенностью и рассмотрим следующее выражение:
var Add = (BatchSize - 1) / BatchSize;
Числитель у нас всегда меньше знаменателя, поэтому результат целочисленного деления всегда будет равен 0
.
Добавим это выражение к частному TotalCount / BatchSize
:
var PageCount = TotalCount / BatchSize + (BatchSize - 1) / BatchSize;
По правилам целочисленной арифметики второе слагаемое всегда равно 0
и оно не влияет на сумму.
Давайте внесем его под общий знаменатель:
var PageCount = (TotalCount + BatchSize - 1) / BatchSize;
Это и есть наша элегантная формула деления с округлением вверх. Почему она работает?
Давайте рассмотрим 2 случая:
TotalCount
делится нацело наBatchSize
, тогдаTotalCount / BatchSize
даст нужный намPageCount
, а(BatchSize - 1) / BatchSize
даст0
и не повлияет на результат.TotalCount
при делении наBatchSize
даёт остатокR
, тогдаTotalCount / BatchSize
даст значениеPageCount - 1
. И у нас остаётся выражение(R + BatchSize - 1) / BatchSize
, гдеR
по определению всегда больше1
и меньшеBatchSize
. При любых значенияхR
результат целочисленного деления будет равен1
. Таким образом, в итоге получим значениеPageCount
.
Бенчмарк
Давайте ряди любопытства сравним все 3 способа.
Код бенчмарка. Нажмите, чтобы развернуть.
using BenchmarkDotNet.Attributes;
namespace Paging;
public class Benchmark
{
[Params(99)]
public int TotalCount;
[Params(10)]
public int BatchSize;
[Params(10000)]
public int Iterations;
[Benchmark]
public int MathCeiling()
{
var result = 0;
for (int i = 0; i < Iterations; i++)
result += (int)Math.Ceiling((double)TotalCount / BatchSize);
return result;
}
[Benchmark]
public int Mod()
{
var result = 0;
for (int i = 0; i < Iterations; i++)
result += TotalCount / BatchSize + (TotalCount % BatchSize == 0 ? 0 : 1);
return result;
}
[Benchmark]
public int Div()
{
var result = 0;
for (int i = 0; i < Iterations; i++)
result += (TotalCount + BatchSize - 1) / BatchSize;
return result;
}
}
Результаты:
BenchmarkDotNet v0.15.2, Windows 10 (10.0.19045.6093/22H2/2022Update)
AMD Ryzen 7 7840H with Radeon 780M Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.412
[Host] : .NET 8.0.18 (8.0.1825.31117), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
DefaultJob : .NET 8.0.18 (8.0.1825.31117), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
| Method | TotalCount | BatchSize | Iterations | Mean | Error | StdDev |
|-------------|------------|-----------|------------|---------:|----------:|----------:|
| MathCeiling | 99 | 10 | 10000 | 2.298 us | 0.0042 us | 0.0037 us |
| Mod | 99 | 10 | 10000 | 4.517 us | 0.0597 us | 0.0559 us |
| Div | 99 | 10 | 10000 | 2.285 us | 0.0455 us | 0.0425 us |
Еще одно подтверждение бессмысленности этих измерений - это необходимость нескольких тысяч итераций, иначе значения неотличимы от нуля. Тем не менее удивительно, что первый и третий варианты показали практически идентичный результат.
Заключение
Данная формула - хорошая демонстрация особенностей целочисленной арифметики, которая немного отличается от обычной. Можно пользоваться этими возможностями, но не в ущерб понятности.
И да, важно, что данная формула работает только для положительных чисел.