relative-placement.js 6.72 KB
/**
 * Created by Techniv on 30/11/2016.
 * @module relativePlacement
 */
(function (definition) {

    // Node export
    if(typeof global != 'undefined' && typeof module != 'undefined' && module.exports) return module.exports = definition();
    //Browser export
    if(typeof window != 'undefined'){
        if(typeof angular != 'undefined' && angular.version.major == 1) return angular.module('relative-placement-js').factory('RelativePlacement', definition);
        if(typeof requirejs != 'undefined') return define(definition());

        window.RelativePlacement = definition();
    }
})(function(){

    /**
     * @name RelativePlacement
     * @constructor
     * @property {Candidate[]} candidateList
     * @property {String[][]} votes
     * @property {Number} numberOfCandidates
     *
     */
    function RelativePlacement() {
        Object.defineProperties(this, {
            candidateList:      {value: {}},
            votes:              {value: []},
            numberOfCandidates: {get: () => Object.keys(this.candidateList).length}
        });
    }

    RelativePlacement.version = '0.0.0';

    /**
     * @name addCandidate
     * @param {String} candidateName
     * @methodOf RelativePlacement
     * @this RelativePlacement
     */
    RelativePlacement.prototype.addCandidate = function addCandidate(candidateName){
        if(this.votes.length) throw new Error("Adding candidates after votes is not permit.");
        if(this.candidateList[candidateName]) throw new Error('"' +candidateName+ '" already exist in candidate list.');
        this.candidateList[candidateName] = new Candidate(candidateName);
    };

    /**
     * @name addCandidates
     * @param {String[]} candidateNames
     * @methodOf RelativePlacement
     * @this RelativePlacement
     */
    RelativePlacement.prototype.addCandidates = function (candidateNames) {
        for(let i = 0; i < candidateNames.length; i++){
            this.addCandidate(candidateNames[i]);
        }
    };

    /**
     * @name addVote
     * @param {String[]} vote
     * @methodOf RelativePlacement
     * @this RelativePlacement
     */
    RelativePlacement.prototype.addVote = function (vote) {
        if(vote.length != this.numberOfCandidates)
            throw new Error('The vote and the candidate list have not the same size.');

        var candidates = Object.keys(this.candidateList);
        var controlMask = Math.pow(2,candidates.length)-1;
        var checkMask = 0;

        for(let i = 0; i < vote.length; i++){
            var index = candidates.indexOf(vote[i]);
            if(index == -1) throw new Error('The candidate "'+vote[i]+"' is not present in candidate list.");
            checkMask |= Math.pow(2, index);
        }

        if(checkMask != controlMask) throw new Error('Candidate must be unique in each vote.');

        this.votes.push(vote);
        for(let i = 0; i < vote.length; i++){
            let candidate = this.candidateList[vote[i]];
            candidate.votes.push(i);
        }
    };

    /**
     *
     * @param {String[][]} votes
     */
    RelativePlacement.prototype.addVotes = function (votes) {
        votes.forEach(vote => this.addVote(vote));
    };
    RelativePlacement.prototype.getResult = function () {
        return relativePlacement(this.votes, this.candidateList);
    };

    /**
     *
     * @param {String} name
     * @constructor
     * @property {String} name
     * @property {Number[]} votes
     * @property {Number[]} placements
     * @property {Number[]} cumulativePlacement
     */
    function Candidate(name) {
        Object.defineProperties(this, {
            name:                   {value: name,   enumerable: true},
            votes:                  {value:[],      enumerable: true},
            placements:             {value:[],      enumerable: true, writable: true},
            cumulativePlacement:    {value:[],      enumerable: true, writable: true}
        });
    }

    /**
     * @name relativePlacement
     * @memberOf RelativePlacement
     * @static
     * @param {String[][]} votes
     * @param {Object<String,Candidate>} [candidates]
     * @returns {String[]}
     */
    function relativePlacement(votes, candidates){
        if(votes.length <= 1) return votes[0];

        // Prepare candidate list if not pass
        var init = false, majority, result, candidateNames;
        if(!candidates) {
            candidates = {};
            init = true;
        }
        candidateNames = votes[0];
        majority = Math.floor(votes.length / 2) + 1;
        result = [];

        candidateNames.forEach((name) => {
            if(init) candidates[name] = new Candidate(name);
            candidates[name].placements[candidateNames.length-1] = 0;
            candidates[name].placements.fill(0,0,candidateNames.length-1);
            candidates[name].cumulativePlacement[candidateNames.length-1] = 0;
            candidates[name].cumulativePlacement.fill(0,0,candidateNames.length-1);
        });

        // Assign votes
        votes.forEach((vote,num) => {
            vote.forEach((name, place) => {
                var candidate = candidates[name];
                candidate.votes[num] = place;
                for(let i = place; i < candidate.placements.length; i++){
                    candidate.placements[i]++;
                    candidate.cumulativePlacement[i] += place + 1;
                }
            });
        });

        // Search placement
        var cursor = 0, nextPlace = 0, proposed = [];
        while(result.length < candidateNames.length && cursor < candidateNames.length){

            // Search majority
            candidateNames.forEach(name => {
                if(result.indexOf(name) != -1) return;
                var candidate = candidates[name];
                if(candidate.placements[cursor] >= majority) proposed.push(candidate);
            });

            if(proposed.length){
                proposed.sort((a,b) => {
                    var sortCursor = cursor;
                    while(sortCursor < candidateNames.length){
                        if(a.placements[sortCursor] != b.placements[sortCursor])
                            return b.placements[sortCursor] - a.placements[sortCursor];
                        if(a.cumulativePlacement[sortCursor] != b.cumulativePlacement[sortCursor])
                            return a.cumulativePlacement[sortCursor] - b.cumulativePlacement[sortCursor];

                        sortCursor++;
                    }
                    return 0;
                });
                result = result.concat(proposed.map(candidate => candidate.name));
                proposed.splice(0);
                continue;
            }

            cursor++;
        }


        return result;
    }

    RelativePlacement.relativePlacement = relativePlacement;
    return RelativePlacement;
});