Last active
April 24, 2020 13:01
-
-
Save MSevey/618b8aa271fc6c250f00c028d7bccdf1 to your computer and use it in GitHub Desktop.
parsePercentages is an implementation of a rounding algorithm that ensures floating point values are rounded in a way that the total adds up to 100%. Additionally this algorithm ensures the order is preserved.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// parsePercentages takes a range of floats and returns them rounded to | |
// percentages that add up to 100. They will be returned in the same order that | |
// they were provided | |
func parsePercentages(values []float64) []float64 { | |
// Create a slice of percentInfo to track information of the values in the | |
// slice and calculate the subTotal of the floor values | |
type percentInfo struct { | |
index int | |
floorVal float64 | |
originalVal float64 | |
} | |
var percentages []*percentInfo | |
var subTotal float64 | |
for i, v := range values { | |
fv := math.Floor(v) | |
percentages = append(percentages, &percentInfo{ | |
index: i, | |
floorVal: fv, | |
originalVal: v, | |
}) | |
subTotal += fv | |
} | |
// Sanity check that all values were added. | |
if len(percentages) != len(values) { | |
// Log critical message but don't return as only a UX bug | |
build.Critical("Not all values added to percentage slice; potential duplicate value error") | |
} | |
// Determine the difference to 100 from the subTotal of the floor values | |
diff := 100 - subTotal | |
// Diff should always be smaller than the number of values. Sanity check for | |
// developers, fine to continue through in production as result will only be | |
// a minor UX descrepency | |
if int(diff) > len(values) { | |
build.Critical(fmt.Errorf("Unexpected diff value %v, number of values %v", diff, len(values))) | |
} | |
// Sort the slice based on the size of the decimal value | |
sort.Slice(percentages, func(i, j int) bool { | |
_, a := math.Modf(percentages[i].originalVal) | |
_, b := math.Modf(percentages[j].originalVal) | |
return a > b | |
}) | |
// Divide the diff amongst the floor values from largest decimal value to | |
// the smallest to decide which values get rounded up. | |
for _, pi := range percentages { | |
if diff <= 0 { | |
break | |
} | |
pi.floorVal++ | |
diff-- | |
} | |
// Reorder the slice and return | |
for _, pi := range percentages { | |
values[pi.index] = pi.floorVal | |
} | |
return values | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment