Source: compute/socialChoiceMethods/allocatedScore.js

/** @module */

import { range } from '@paretoman/votekit-utilities'
import * as types from '@paretoman/votekit-types'

/**
 * This method has rounds.
 * Each round, the votes are weighted based on the previous round.
 * Weights start at 1.
 * Each round, the candidate with the highest weighted tally is selected.
 * Then the scores for the candidate are sorted.
 * The voters' weight is spent, starting from the top scores.
 * Weight is spent until the weight of voters included is equal to 1/seats, which is the quota.
 * @param {types.typesVotes.votes} votes - The object for vote data.
 * @param {types.typesSocialChoice.socialChoiceOptions} socialChoiceOptions - options to specify a social choice function.
 * @returns {types.typesSocialChoice.socialChoiceResults} - the results returned from a social choice function.
 */
export default function allocatedScore(votes, socialChoiceOptions) {
    const { voteFractions } = votes.preferenceTallies
    const { scoreVotes } = votes.preferenceLists

    const { seats } = socialChoiceOptions

    const nk = scoreVotes[0].length // number of candidates
    const nv = scoreVotes.length // number of votes
    const quota = 1 / seats // fraction of voters in a quota

    const weight = Array(nv).fill(1)
    const allocation = Array(nk).fill(0)

    let nw = 0 // number of winners
    for (let r = 0; r < nk; r++) {
        if (nw >= seats) break

        // Select Winner //
        // add up number of votes for each candidate and choose the one with the most

        const tally = Array(nk).fill(0)
        for (let k = 0; k < nk; k++) {
            if (allocation[k] === 1) continue // no clones
            for (let i = 0; i < nv; i++) {
                tally[k] += weight[i] * voteFractions[i] * scoreVotes[i][k]
            }
        }

        const max = Math.max(...tally)
        const iWinner = tally.indexOf(max)

        allocation[iWinner] = 1
        nw += 1

        // Adjust Weight //

        // sort scores for winner, find indices of sorted scores
        // sort descending
        const iSortVoters = range(nv).sort(
            (a, b) => scoreVotes[b][iWinner] - scoreVotes[a][iWinner],
        )

        // add up to a quota
        // remove weight from votes inside quota
        // sumTop is the sum of the top scores that the winner received.
        let sumTop = 0
        for (let i = 0; i < nv; i++) {
            const index = iSortVoters[i]
            const score = scoreVotes[index][iWinner]
            sumTop += score * weight[index] * voteFractions[i]
            weight[index] = 0 // remove vote
            if (sumTop >= quota) {
                break
            }
        }
        // Todo: deal with when scores are the same.
        // Todo: deal with sumTop not exactly equal to quota.
    }

    const socialChoiceResults = { allocation }
    return socialChoiceResults
}

/** @constant {object} - an object: this function and descriptions of its name, input, and output */
export const allocatedScoreMetadata = {
    name: 'Allocated Score',
    shortName: 'AllocScore',
    functionName: 'allocatedScore',
    voteCasterName: 'scoreFull', // for input
    socialChoiceType: 'multiWinner',
    elect: allocatedScore,
}