Skip to content

Commit

Permalink
Merge pull request #13554 from nextcloud/fix/11867/poll-rounding
Browse files Browse the repository at this point in the history
  • Loading branch information
Antreesy authored Oct 29, 2024
2 parents 0268a1c + a17b043 commit 089172e
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 9 deletions.
17 changes: 8 additions & 9 deletions src/components/PollViewer/PollViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
<div class="results__option-title">
<p>{{ option }}</p>
<p class="percentage">
{{ getVotePercentage(index) + '%' }}
{{ votePercentage[index] + '%' }}
</p>
</div>
<div v-if="getFilteredDetails(index).length > 0 || selfHasVotedOption(index)"
Expand All @@ -52,7 +52,7 @@
</p>
</div>
<NcProgressBar class="results__option-progress"
:value="getVotePercentage(index)"
:value="votePercentage[index]"
size="medium" />
</div>
</div>
Expand Down Expand Up @@ -140,6 +140,7 @@ import { POLL } from '../../constants.js'
import { hasTalkFeature } from '../../services/CapabilitiesManager.ts'
import { EventBus } from '../../services/EventBus.ts'
import { usePollsStore } from '../../stores/polls.ts'
import { calculateVotePercentage } from '../../utils/calculateVotePercentage.ts'
import { convertToJSONDataURI } from '../../utils/fileDownload.ts'

export default {
Expand Down Expand Up @@ -265,6 +266,11 @@ export default {
canEndPoll() {
return this.isPollOpen && this.selfIsOwnerOrModerator
},

votePercentage() {
const votes = Object.keys(Object(this.poll?.options)).map(index => this.poll?.votes['option-' + index] ?? 0)
return calculateVotePercentage(votes, this.poll.numVoters)
},
},

watch: {
Expand Down Expand Up @@ -381,13 +387,6 @@ export default {
getFilteredDetails(index) {
return (this.poll?.details || []).filter(item => item.optionId === index)
},

getVotePercentage(index) {
if (!this.poll?.votes['option-' + index] || !this.poll?.numVoters) {
return 0
}
return parseInt(this.poll?.votes['option-' + index] / this.poll?.numVoters * 100)
},
},
}
</script>
Expand Down
39 changes: 39 additions & 0 deletions src/utils/__tests__/calculateVotePercentage.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { calculateVotePercentage } from '../calculateVotePercentage.ts'

describe('calculateVotePercentage', () => {
const tests = [
[0, [], 0],
[1, [1], 100],
// Math rounded to 100%
[4, [1, 3], 100],
[11, [1, 2, 8], 100],
[13, [11, 2], 100],
[13, [9, 4], 100],
[26, [16, 5, 5], 100],
// Rounded to 100% by largest remainder
[1000, [132, 494, 92, 282], 100],
[1000, [135, 480, 97, 288], 100],
// Best effort is 99%
[3, [1, 1, 1], 99],
[7, [2, 2, 3], 99],
[1000, [133, 491, 93, 283], 99],
[1000, [134, 488, 94, 284], 99],
// Best effort is 98%
[1000, [136, 482, 96, 286], 98],
[1000, [135, 140, 345, 95, 285], 98],
// Best effort is 97%
[1000, [137, 132, 347, 97, 287], 97],
]

it.each(tests)('test %d votes in %o distribution rounds to %d%%', (total, votes, result) => {
const percentageMap = calculateVotePercentage(votes, total)

expect(votes.reduce((a, b) => a + b, 0)).toBe(total)
expect(percentageMap.reduce((a, b) => a + b, 0)).toBe(result)
})
})
72 changes: 72 additions & 0 deletions src/utils/calculateVotePercentage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Finds indexes of largest remainders to distribute quota
* @param array array of numbers to compare
*/
function getLargestIndexes(array: number[]) {
let maxValue = 0
const maxIndexes: number[] = []

for (let i = 0; i < array.length; i++) {
if (array[i] > maxValue) {
maxValue = array[i]
maxIndexes.length = 0
maxIndexes.push(i)
} else if (array[i] === maxValue) {
maxIndexes.push(i)
}
}

return maxIndexes
}

/**
* Provide percentage distribution closest to 100 by method of largest remainder
* @param votes array of given votes
* @param total amount of votes
*/
export function calculateVotePercentage(votes: number[], total: number) {
if (!total) {
return votes
}

const rounded: number[] = []
const wholes: number[] = []
const remainders: number[] = []
let sumRounded = 0
let sumWholes = 0

for (const i in votes) {
const quota = votes[i] / total * 100
rounded.push(Math.round(quota))
wholes.push(Math.floor(quota))
remainders.push(Math.round((quota % 1) * 1000))
sumRounded += rounded[i]
sumWholes += wholes[i]
}

// Check if simple round gives 100%
if (sumRounded === 100) {
return rounded
}

// Increase values by largest remainder method if difference allows
for (let i = 100 - sumWholes; i > 0;) {
const largest = getLargestIndexes(remainders)
if (largest.length > i) {
return wholes
}

for (const idx of largest) {
wholes[idx]++
remainders[idx] = 0
i--
}
}

return wholes
}

0 comments on commit 089172e

Please sign in to comment.