The cheese cake is precut in 12 slices, and you have played with your friends to win the largest share.
The method is, of course, to sum all points achieved and then calculate the share for each player. However, that results in decimal values, meaning that the slices should be sub-sliced and we don't want that. Thus, the shares are rounded to full slices.
The second column shows the points achieved. This is the left graph above.
Then follows the players' shares of the cake and how many slices that equals.
The slices are rounded to obtain the requested count of full slices, but these add up to 13. Thus, one slice has to go, but which? There are some options:
The answer is to reduce where the impact will be smallest. We have two players with three slices, but player 5's count has already been rounded down. Reducing player 1's count of slices will still leave him or here two slices.
Two players have been rounded up to one slice. Reducing either of these to zero will have a very large impact, 100%, on the share.
Thus player 1 is the candidate to give up a slice.
The last column lists the final count of slices, adding up to the twelve available. This is the right graph above.
Name |
points
|
share | slices | requested | rounded |
---|---|---|---|---|---|
Player 1 | 33 | 0.2171 | 2.605 | 3 | 2 |
Player 2 | 9 | 0.0592 | 0.711 | 1 | 1 |
Player 3 | 13 | 0.0855 | 1.026 | 1 | 1 |
Player 4 | 22 | 0.1447 | 1.737 | 2 | 2 |
Player 5 | 41 | 0.2697 | 3.237 | 3 | 3 |
Player 6 | 11 | 0.0724 | 0.868 | 1 | 1 |
Player 7 | 23 | 0.1513 | 1.816 | 2 | 2 |
Total | 12.000 | 13 | 12 |
In this small example, the result is easy to figure out. But for a larger count of players - or other items like invoice lines or measurements - it will be difficult to figure out the optimum result. You will need a function to carry it out.
Let's take another example where the outcome may not be that obvious. Here, four small values are added with one much larger. The sum of the rounded values (1443) differs with 2 from the rounded total (1441).
But which values should be corrected?
You may argue, that the largest value(s) should be corrected, as the imposed error will be the smallest relative to the value. But it would be regarded as strange, that (here) the only value that is hardly rounded would be the one to adjust while the other values, that are rounded up or down quite a lot, should remain.
However, you could just as well argue, that those values with the largest difference in the rounded value should be adjusted. In the table, that is seen in column Difference. That would call for an adjustment of the values 3 and 2, but leaves the question, it not the value of 4 more likely should have been adjusted.
There is no single right or wrong method to follow, but a good compromise that will work well in most cases is to weigh the difference meaning multiplying the value and the difference.
Those values are listed in the next column. Here you'll see, that the large value due to its very small difference results in with a low figure and that the smallest values, 0.6 rounded to 1, also results in a low figure due to the very low input value. The two at the top (highlighted) will then be those to adjust:
ITEM | INPUT | ROUNDED | DIFFERENCE | WEIGHTED DIFFERENCE | RESULT |
---|---|---|---|---|---|
1 | 1432.99999 | 1433 | 0.00001 | 0.0143 | 1433 |
2 | 2.52 | 3 | 0.48 | 1.2096 | 2 |
3 | 1.51 | 2 | 0.49 | 0.7399 | 2 |
4 | 3.55 | 4 | 0.45 | 1.5975 | 3 |
5 | 0.6 | 1 | 0.4 | 0.2400 | 1 |
Total | 1441.17999 | 1443 | 1441 |
The final values are listed in column Result, and the total is seen to match the rounded total of the input values.
In some situations, there may be several identical input values, and the rounded total doesn't match the sum of the rounded values. So which value to adjust, if adjusting all would generate another mismatch?
You could decide for picking an arbitrary item to adjust. But that is no good solution because several calls of the function with identical input values then might generate different result sets. Thus, a simple rule has been set up:
If one of several identical values must be adjusted, pick the first.
An example is shown here:
ITEM | INPUT | ROUNDED | DIFFERENCE | WEIGHTED DIFFERENCE | RESULT |
---|---|---|---|---|---|
1 | 1.6667 | 1.67 | 0.0033 | 0.00550011 | 1.67 |
2 | 34.6667 | 34.67 | 0.0033 | 0.11440011 | 34.66 |
3 | 5.3333 | 5.33 | -0.0033 | 0.01759989 | 5.33 |
4 | 34.6667 | 34.67 | 0.0033 | 0.11440011 | 34.67 |
5 | 5.3333 | 5.33 | -0.0033 | 0.01759989 | 5.33 |
6 | 5.3333 | 5.33 | -0.0033 | 0.01759989 | 5.33 |
7 | 1.6667 | 1.67 | 0.0033 | 0.00550011 | 1.67 |
8 | 34.6667 | 34.67 | 0.0033 | 0.11440011 | 34.67 |
Total | 123.3334 | 123.34 | 123.33 |
Note, that item 2 has been adjusted down by 0.01 for the sum to match the rounded 123.3334.
A function to do this for us could take as input an array of the items or values to round and return an array with the rounded values.
This basic method would:
In addition to basic rounding, the function provided here will:
That's quite a few steps and conditions to take care of, thus the function can appear a little long-winded, even though the code is extensively commented in-line. So let's briefly walk through the steps.
The first thing to check is if an array is passed. If so, this is looped and the values are summed as Decimal, If values are very large, the function falls back to use Double.
' Raise error if an array is not passed.
Item = UBound(Values)
' Ignore errors while summing the values.
On Error Resume Next
If Err.Number = 0 Then
' Try to sum the passed values as a Decimal.
Sum = CDec(0)
For Item = LBound(Values) To UBound(Values)
If IsNumeric(Values(Item)) Then
Sum = Sum + CDec(Values(Item))
If Err.Number <> 0 Then
' Values exceed range of Decimal.
' Exit loop and try using Double.
Exit For
End If
End If
Next
End If
If Err.Number <> 0 Then
' Try to sum the passed values as a Double.
Err.Clear
Sum = CDbl(0)
For Item = LBound(Values) To UBound(Values)
If IsNumeric(Values(Item)) Then
Sum = Sum + CDbl(Values(Item))
If Err.Number <> 0 Then
' Values exceed range of Double.
' Exit loop and raise error.
Exit For
End If
End If
Next
End If
' Collect the error number as "On Error Goto 0" will clear it.
ErrorNumber = Err.Number
On Error GoTo 0
If ErrorNumber <> 0 Then
' Extreme values. Give up.
Error.Raise ErrorNumber
End If
Now we have the rounded values and the sum.
Next step it to round the requested total so we know what to aim for. Note, that if either the sum of the values or the total is zero, no scaling is possible as it would imply a division by zero. Also, the sign is recorded for later use.
You will notice, that function RoundMid is used here. This is a precision rounding function you can study here:
Rounding values up, down, by 4/5, or to significant figures
' Correct a missing or invalid parameter value for Total.
If Not IsNumeric(Total) Then
Total = 0
End If
If Total = 0 Then
RoundedTotal = 0
Else
' Round Total to an appropriate data type.
' Set data type of RoundedTotal to match Sum.
Select Case VarType(Sum)
Case vbSingle, vbDouble
Value = CDbl(Total)
Case Else
Value = CDec(Total)
End Select
RoundedTotal = RoundMid(Value, NumDigitsAfterDecimal)
End If
' Calculate scaling factor and sign.
If Sum = 0 Or RoundedTotal = 0 Then
' Cannot scale a value of zero.
Sign = 1
Ratio = 1
Else
Sign = Sgn(Sum) * Sgn(RoundedTotal)
' Ignore error and convert to Double if total exceeds the range of Decimal.
On Error Resume Next
Ratio = Abs(RoundedTotal / Sum)
If Err.Number <> 0 Then
RoundedTotal = CDbl(RoundedTotal)
Ratio = Abs(RoundedTotal / Sum)
End If
On Error GoTo 0
End If
Now we have the basic bits and pieces and can start creating the output.
For the output, we create two arrays - one to hold the values and one to hold the sorting of these.
The first we fill here, later we fill the array for sorting.
' Create array to hold the rounded values.
RoundedValues = Values
' Scale and round the values and sum the rounded values.
' Variables will get the data type of RoundedValues.
' Ignore error and convert to Double if total exceeds the range of Decimal.
On Error Resume Next
For Item = LBound(Values) To UBound(Values)
RoundedValues(Item) = RoundMid(Values(Item) * Ratio, NumDigitsAfterDecimal)
If RoundedValues(Item) > 0 Then
PlusSum = PlusSum + Values(Item)
RoundedPlusSum = RoundedPlusSum + RoundedValues(Item)
If Err.Number <> 0 Then
RoundedPlusSum = CDbl(RoundedPlusSum) + CDbl(RoundedValues(Item))
End If
Else
MinusSum = MinusSum + Values(Item)
RoundedMinusSum = RoundedMinusSum + RoundedValues(Item)
If Err.Number <> 0 Then
RoundedMinusSum = CDbl(RoundedMinusSum) + CDbl(RoundedValues(Item))
End If
End If
Next
RoundedSum = RoundedPlusSum + RoundedMinusSum
If Err.Number <> 0 Then
RoundedPlusSum = CDbl(RoundedPlusSum)
RoundedMinusSum = CDbl(RoundedMinusSum)
RoundedSum = RoundedPlusSum + RoundedMinusSum
End If
On Error GoTo 0
If RoundedTotal = 0 Then
' No total is requested.
' Use as total the rounded sum of the passed unrounded values.
RoundedTotal = RoundMid(Sum, NumDigitsAfterDecimal)
End If
Note, that positive and negative values are summed separately to be able to take care of special cases as seen next.
This is the crucial part where we use the array to hold the sort order of the values.
First a third array, SortingValues, is created and filled with the calculated values to sort on. Note, that the values are made absolute.
These two arrays are then sorted by an external function, QuickSortIndex, which is a traditional quick sort function modified to leave the source array untouched and leave the sorting order in the array for the sort order.
The sorting is used to order the values after the relative error the rounding of these will cause. This may seem cryptic, but those values are used later to determine the order in which values should be corrected up or down to make the sum of the rounded values match the requested total.
In case of a mismatch, the difference is calculated and distributed among the values following the sort order found.
Note, that if a value is corrected and an identical value reversely signed exists, this is corrected as well. This way, if two values were intended to outweigh each other, they will still do so after a correction.
Just about every step is documented by the in-line comments.
' Check if a correction of the rounded values is needed.
If (RoundedPlusSum + RoundedMinusSum = 0) And (RoundedTotal = 0) Then
' All items are rounded to zero. Nothing to do.
' Return zero.
ElseIf RoundedSum = RoundedTotal Then
' Match. Nothing more to do.
ElseIf RoundedSum = Sign * RoundedTotal Then
' Match, except that values shall be reversely signed.
' Will be done later before exit.
Else
' Correction is needed.
' Redim array to hold the sorting of the rounded values.
ReDim SortedItems(LBound(Values) To UBound(Values))
' Fill array with default sorting.
For Item = LBound(SortedItems) To UBound(SortedItems)
SortedItems(Item) = Item
Next
' Create array to hold the values to sort.
SortingValues = RoundedValues
' Fill the array after the relative rounding error and - for items with equal rounding error - the
' size of the value of items.
For Item = LBound(SortedItems) To UBound(SortedItems)
If Values(SortedItems(Item)) = 0 Then
' Zero value.
SortValue = 0
ElseIf RoundedPlusSum + RoundedMinusSum = 0 Then
' Values have been rounded to zero.
' Use original values.
SortValue = Values(SortedItems(Item))
ElseIf VarType(Values(SortedItems(Item))) = vbDouble Then
' Calculate relative rounding error.
' Value is exceeding Decimal. Use Double.
SortValue = (Values(SortedItems(Item)) * Ratio - CDbl(RoundedValues(SortedItems(Item)))) * (Values(SortedItems(Item)) / Sum)
Else
' Calculate relative rounding error using Decimal.
SortValue = (Values(SortedItems(Item)) * Ratio - RoundedValues(SortedItems(Item))) * (Values(SortedItems(Item)) / Sum)
End If
' Sort on the absolute value.
SortingValues(Item) = Abs(SortValue)
Next
' Sort the array after the relative rounding error and - for items with equal rounding error - the
' size of the value of items.
QuickSortIndex SortedItems, SortingValues
' Distribute a difference between the rounded sum and the requested total.
If RoundedPlusSum + RoundedMinusSum = 0 Then
' All rounded values are zero.
' Set Difference to the rounded total.
Difference = RoundedTotal
Else
Difference = Sgn(RoundedSum) * (Abs(RoundedTotal) - Abs(RoundedSum))
End If
' If Difference is positive, some values must be rounded up.
' If Difference is negative, some values must be rounded down.
' Calculate Delta, the value to increment/decrement by.
Delta = Sgn(Difference) * 10 ^ -NumDigitsAfterDecimal
' Loop the rounded values and increment/decrement by Delta until Difference is zero.
For Item = UBound(SortedItems) To LBound(SortedItems) Step -1
' If values should be incremented, ignore values rounded up.
' If values should be decremented, ignore values rounded down.
If Sgn(Difference) = Sgn(Values(SortedItems(Item)) * Ratio - RoundedValues(SortedItems(Item))) Then
' Adjust this item.
RoundedValues(SortedItems(Item)) = RoundedValues(SortedItems(Item)) + Delta
If Item > LBound(SortedItems) Then
' Check if the next item holds the exact reverse value.
If Values(SortedItems(Item)) = -Values(SortedItems(Item - 1)) Then
' Adjust the next item as well to avoid uneven incrementing.
Item = Item - 1
RoundedValues(SortedItems(Item)) = RoundedValues(SortedItems(Item)) - Delta
Difference = Difference + Delta
End If
End If
Difference = Difference - Delta
End If
If Difference = 0 Then
Exit For
End If
Next
End If
That nearly completes the task.
This final step is where the recorded value of Sign is checked and applied. If needed, the resulting array is simply looped, and the values are reversed.
If Sign = -1 Then
' The values shall be reversely signed.
For Item = LBound(RoundedValues) To UBound(RoundedValues)
RoundedValues(Item) = -RoundedValues(Item)
Next
End If
' Return the rounded total.
Total = RoundedTotal
' Return the array holding the rounded values.
RoundSum = RoundedValues
Finally, the processed array with the resulting values is returned.
Also, the rounded total - declared ByRef - is returned by reference.
The function takes three arguments and returns an array:
Public Function RoundSum( _
ByVal Values As Variant, _
Optional ByRef Total As Variant, _
Optional ByVal NumDigitsAfterDecimal As Long) _
As Variant
Of these, Values is the input array, Total is the total value to return, and NumDigitsAfterDecimal determines how the values are rounded. If positive, rounding will be with decimals, if zero rounding will be to integer values, and - if negative - rounding will be to 10, 100, 1000, etc.
If Total is not specified or is zero, the values will be rounded to have a sum that equals the rounded sum of the unrounded values.
The usage is quite simple:
' Create an array for the example input values.
Values = Array(1.66, 1.66, 1.67, -1.7, 1.66)
' Specify the requested total (optional).
RequestedTotal = -11.12
' Specify the rounding, here with one decimal.
NumDigitsAfterDecimal = 1
' This will entail an expected (rounded) total of -11.1.
' Call the function with the parameters and return an array.
Result = RoundSum(Values, Total, NumDigitsAfterDecimal)
' Now, array Result will hold the rounded values, and Total the rounded total.
' If listed, the rounded values and total will be:
'
' Item Result
' 0 -3.7
' 1 -3.7
' 2 -3.8
' 3 3.8
' 4 -3.7
' Total -11.1
Note that the signs are reversed, as we specified a negative total while the sum of the values was positive.
' Round the values of one field of a recordset and write the rounded values (matching the total)
' to another field of the recordset.
'
' Example:
' Rounding the values to two decimals for the sum to match the original sum:
'
' RoundRecordSum CurrentDb.OpenRecordset("Select Value, RoundedValue From SomeTable"), 0, 2
'
' 2024-03-11. Gustav Brock, Cactus Data ApS, CPH.
'
Public Sub RoundRecordSum( _
ByRef Records As DAO.Recordset, _
ByVal Total As Double, _
Optional ByVal NumDigitsAfterDecimal As Long)
Dim Rows As Variant
Dim Values() As Double
Dim Value As Double
Dim Sum As Double
Dim RecordCount As Long
Dim Index As Long
If Records.RecordCount = 0 Then
Exit Sub
End If
' Count records.
Records.MoveLast
Records.MoveFirst
RecordCount = Records.RecordCount
' Retrieve multi-dimensional array.
Rows = Records.GetRows(RecordCount)
' Read values from the first field of the recordset.
' Convert to one-dimensional array and calculate the sum.
ReDim Values(0 To UBound(Rows, 2))
For Index = LBound(Values) To UBound(Values)
Value = Rows(0, Index)
Values(Index) = Value
Sum = Sum + Value
Debug.Print Value
Next
Debug.Print "Sum:", Sum
' Adjust sum of output if needed.
If Total = 0 Then
Total = Sum
End If
' Round the array of values.
Values = RoundSum(Values, Total, NumDigitsAfterDecimal)
' Write the values to the second field of the recordset.
Total = 0
Records.MoveFirst
For Index = LBound(Values) To UBound(Values)
Value = Values(Index)
If Records(1).Value <> Value Then
Records.Edit
Records(1).Value = Value
Records.Update
End If
Total = Total + Value
Records.MoveNext
Next
Debug.Print "Total:", Total
End Sub
As mentioned above, another article about demanding rounding is online. This covers basic rounding:
Rounding values up, down, by 4/5, or to significant figures
An extended discussion can be found on Wikipedia:
Rounding of summands preserving the total
The current version can always be found at GitHub VBA.Round
The version for this article is here: Rounding 1.4.4.zip. This includes a Microsoft Access 365 project.
I hope you found this article useful. You are encouraged to ask questions, report any bugs or make any other comments about it below.
Note: If you need further "Support" about this topic, please consider using the Ask a Question feature of Experts Exchange. I monitor questions asked and would be pleased to provide any additional support required in questions asked in this manner, along with other EE experts.
Please do not forget to press the "Thumbs Up" button if you think this article was helpful and valuable for EE members.
Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.
Comments (0)