Skip to content

Instantly share code, notes, and snippets.

@MSevey
Last active April 24, 2020 13:01
Show Gist options
  • Save MSevey/618b8aa271fc6c250f00c028d7bccdf1 to your computer and use it in GitHub Desktop.
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.
// 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