An iOS statistics screen shows three equally weighted items as 33% each, yet the total is only 99%. Assigning the entire shortfall to the last item can give a percentage to an item with zero weight. In a build pipeline on a cloud Mac, you can catch these issues without launching a simulator: extract the allocation algorithm into a pure Swift function, check totals, ordering, and invalid inputs, then have the UI consume the same result.
Define the display units and input contract first
This approach handles nonnegative integer counts, such as completed tasks across three categories—not monetary amounts, floating-point measurements, or percentages that have already been rounded. Set the total number of display units to 100, with each unit representing one percentage point. For one decimal place, use 1000 units instead, with each unit representing one tenth of a percentage point.
The contract has four requirements: the output has the same length as the input; all output values are nonnegative and sum to the total number of units; zero weights receive zero units; and identical inputs always produce identical results. An empty array, negative weights, or a total weight of zero must fail rather than silently produce an equal allocation.
“No data” and “every item has the same share” are different business states. An all-zero input should make the page show an empty state, not let the allocation algorithm disguise it as valid statistics.
Ties in remainders are resolved by input index, so callers must first arrange the data by stable category identifiers. Do not treat dictionary iteration order as a product rule: refreshing the page could otherwise change which item receives an extra unit.
Fill the shortfall with the integer largest remainder method
First, multiply each weight by the total number of units, then divide by the total weight. The quotient is the base quota; the remainder determines the order for distributing extra units. If the base quotas fall short of the required total, add one unit at a time in descending order of remainder.
Because every quota is rounded down, the number of units left to distribute is strictly less than the number of items. There is no need to loop until the values converge or to introduce a floating-point tolerance. Save the following code as percentage-check.swift; it includes both the implementation and a minimal set of checks.
enum AllocationError: Error {
case invalidInput
case overflow
}
func allocate(_ weights: [Int], units: Int = 100) throws -> [Int] {
guard !weights.isEmpty,
(1...10_000).contains(units),
weights.allSatisfy({ $0 >= 0 }) else {
throw AllocationError.invalidInput
}
var total = 0
for weight in weights {
let sum = total.addingReportingOverflow(weight)
guard !sum.overflow else {
throw AllocationError.overflow
}
total = sum.partialValue
}
guard total > 0 else {
throw AllocationError.invalidInput
}
var result = [Int]()
var remainders = [Int]()
for weight in weights {
let product = weight.multipliedReportingOverflow(by: units)
guard !product.overflow else {
throw AllocationError.overflow
}
result.append(product.partialValue / total)
remainders.append(product.partialValue % total)
}
let remaining = units - result.reduce(0, +)
let order = weights.indices.sorted {
if remainders[$0] == remainders[$1] {
return $0 < $1
}
return remainders[$0] > remainders[$1]
}
for index in order.prefix(remaining) {
result[index] += 1
}
return result
}
func expectFailure(_ weights: [Int], units: Int = 100) {
do {
_ = try allocate(weights, units: units)
} catch {
return
}
preconditionFailure("Expected allocation to fail")
}
let equal = try allocate([1, 1, 1])
precondition(equal == [34, 33, 33])
let withZero = try allocate([0, 1, 1])
precondition(withZero == [0, 50, 50])
let tenths = try allocate([1, 1, 1], units: 1000)
precondition(tenths == [334, 333, 333])
for a in 0...12 {
for b in 0...12 {
for c in 0...12 {
let weights = [a, b, c]
let total = weights.reduce(0, +)
if total == 0 { continue }
let output = try allocate(weights)
precondition(output.count == weights.count)
precondition(output.reduce(0, +) == 100)
precondition(output.allSatisfy { $0 >= 0 })
for index in weights.indices {
let floorQuota = weights[index] * 100 / total
precondition(
output[index] == floorQuota ||
output[index] == floorQuota + 1
)
if weights[index] == 0 {
precondition(output[index] == 0)
}
}
}
}
}
expectFailure([])
expectFailure([0, 0])
expectFailure([-1, 2])
expectFailure([1], units: 0)
expectFailure([Int.max, 1])
expectFailure([Int.max])
The code deliberately checks for overflow during both addition and multiplication. Even when the final percentage is small, an intermediate weight product can exceed the integer range. This implementation rejects the input rather than converting to floating point and hiding a change in precision. Supporting larger counts requires a separate design using wider integer arithmetic.
Run standalone acceptance checks on a cloud Mac
First, confirm the current toolchain with xcrun swift --version, then run the commands below. Successful checks produce no output. A failed assertion or an unhandled error causes the process to fail, and the pipeline must preserve that exit status.
set -eu
xcrun swift percentage-check.swift
For a formal pipeline gate, you can compile with xcrun swiftc -Onone and then run the executable. Do not use an optimization mode that disables these assertions. A more robust long-term approach is to put the function in the application module, move the assertions into a test target, and run them through the existing test command. This avoids maintaining separate copies of the algorithm in the script and the product.
The checks enumerate combinations of three weights over a small range; they are not a performance benchmark. They cover inputs near the all-zero case, a single item taking the entire allocation, equal remainders, and small weights rounded down to zero. When adding tests, broaden the shapes of the inputs rather than simply repeating the same uniform sample.
Separate rounding errors from business rules
An easy mistake to miss is losing the mapping to the original items after sorting. This implementation sorts only the indices, so the final array still corresponds to the original items. The UI must not use the remainder ranking as the display order for categories.
Also, do not require “every positive weight must receive at least one display unit.” When there are more categories than display units, that requirement is incompatible with preserving the total. To show very small shares, increase the display precision or use separate wording such as “below the display threshold,” but do not silently change the quotas.
Finally, do not treat this method as a guarantee of monotonicity across datasets. Changing other categories’ weights or the display precision can change the remainder ranking. Tests should verify the quota bounds for the current data, not assert that an item’s allocation can never decrease across refreshes.
Decimal formatting is only for presentation
With 1000 total units, 334 represents 33.4%. If the formatting API expects a proportion between zero and one, pass the value corresponding to 334 / 1000, without multiplying it by one hundred again. The formatting stage must not recalculate each share from the original weights or round the values again to a different precision.
Turn failures into reviewable evidence
Before merging, retain the input weights, total units, base quotas, remainder ranking, and final output. If production statistics may include sensitive categories, reproduce the case with fixed test fixtures rather than writing real business details into build artifacts.
Focus the review on three boundaries: whether invalid input fails explicitly, whether equal remainders are resolved deterministically, and whether UI formatting preserves the chosen precision. Once the checks pass, the data layer should provide only weights, the allocation layer only integer units, and the presentation layer only localized formatting. Keeping these three responsibilities separate makes it possible to trace an incorrect percentage total to a specific stage, rather than masking the problem by adjusting the last item.
Frequently asked questions
Why not round each percentage independently?
Independent rounding does not preserve the total. Three equal weights displayed as 33 percent each add up to 99 percent. Largest-remainder allocation assigns whole quotas first, then distributes the remaining units by remainder size.
Does a larger weight always produce a larger displayed share?
That is not guaranteed across different inputs. Changes to other weights or the unit count can alter remainder rankings. This implementation guarantees the current total, quota bounds and deterministic output, not cross-input monotonicity.
Test your cloud Mac with your own pipeline
ArmRDP provides dedicated physical Mac minis, not virtual machines. Rent by the day, verify your toolchain and build results, then choose a rental term that works for your team.
Rent a Cloud Mac