diff --git a/src/components/PollViewer/PollViewer.vue b/src/components/PollViewer/PollViewer.vue
index 59181712df5..0a8a79da3ee 100644
--- a/src/components/PollViewer/PollViewer.vue
+++ b/src/components/PollViewer/PollViewer.vue
@@ -39,7 +39,7 @@
{{ option }}
- {{ getVotePercentage(index) + '%' }}
+ {{ votePercentage[index] + '%' }}
@@ -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 {
@@ -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: {
@@ -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)
- },
},
}
diff --git a/src/utils/__tests__/calculateVotePercentage.spec.js b/src/utils/__tests__/calculateVotePercentage.spec.js
new file mode 100644
index 00000000000..0fd0251c928
--- /dev/null
+++ b/src/utils/__tests__/calculateVotePercentage.spec.js
@@ -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)
+ })
+})
diff --git a/src/utils/calculateVotePercentage.ts b/src/utils/calculateVotePercentage.ts
new file mode 100644
index 00000000000..c934f7f9355
--- /dev/null
+++ b/src/utils/calculateVotePercentage.ts
@@ -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
+}