Skip to content

Commit

Permalink
Added min/max number of choices in multichoice election
Browse files Browse the repository at this point in the history
  • Loading branch information
marcvelmer committed Oct 31, 2024
1 parent 830c688 commit 40b543b
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 26 deletions.
33 changes: 29 additions & 4 deletions src/types/election/multichoice.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { MultiLanguage } from '../../util/lang';
import { CustomMeta, IElectionParameters, IVoteType } from './election';
import { UnpublishedElection } from './unpublished';
import { Choice, ElectionMetadata, ElectionResultsTypeNames, getElectionMetadataTemplate } from '../metadata';
import {
Choice,
ChoiceProperties,
ElectionMetadata,
ElectionResultsTypeNames,
getElectionMetadataTemplate,
} from '../metadata';
import { Vote } from '../vote';

export interface IMultiChoiceElectionParameters extends IElectionParameters {
maxNumberOfChoices: number;
minNumberOfChoices: number;
canRepeatChoices?: boolean;
canAbstain?: boolean;
}
Expand All @@ -15,6 +22,7 @@ export interface IMultiChoiceElectionParameters extends IElectionParameters {
*/
export class MultiChoiceElection extends UnpublishedElection {
private _canAbstain: boolean;
private _minNumberOfChoices: number;

/**
* Constructs a multi choice election
Expand All @@ -24,6 +32,7 @@ export class MultiChoiceElection extends UnpublishedElection {
public constructor(params: IMultiChoiceElectionParameters) {
super(params);
this.maxNumberOfChoices = params.maxNumberOfChoices;
this.minNumberOfChoices = params.minNumberOfChoices;
this.canRepeatChoices = params.canRepeatChoices ?? false;
this.canAbstain = params.canAbstain ?? false;
}
Expand Down Expand Up @@ -86,19 +95,27 @@ export class MultiChoiceElection extends UnpublishedElection {
(_v, index) => String(index + this.questions[0].choices.length)
),
repeatChoice: this.canRepeatChoices,
numChoices: {
min: this.minNumberOfChoices,
max: this.maxNumberOfChoices,
},
},
};

return super.generateMetadata(metadata);
}

public static checkVote(vote: Vote, voteType: IVoteType): void {
public static checkVote(vote: Vote, voteType: IVoteType, voteProperties: ChoiceProperties): void {
if (voteType.uniqueChoices && new Set(vote.votes).size !== vote.votes.length) {
throw new Error('Choices are not unique');
}

if (voteType.maxCount != vote.votes.length) {
throw new Error('Invalid number of choices');
if (vote.votes.length > voteType.maxCount) {
throw new Error('Invalid number of choices, maximum is ' + voteType.maxCount);
}

if (vote.votes.length < voteProperties.numChoices.min) {
throw new Error('Invalid number of choices, minimum is ' + voteProperties.numChoices.min);
}

vote.votes.forEach((vote) => {
Expand All @@ -116,6 +133,14 @@ export class MultiChoiceElection extends UnpublishedElection {
this.voteType.maxCount = value;
}

get minNumberOfChoices(): number {
return this._minNumberOfChoices;
}

set minNumberOfChoices(value: number) {
this._minNumberOfChoices = value;
}

get canRepeatChoices(): boolean {
return !this.voteType.uniqueChoices;
}
Expand Down
2 changes: 1 addition & 1 deletion src/types/election/published.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export class PublishedElection extends Election {
public checkVote(vote: Vote): void {
switch (this.resultsType?.name) {
case ElectionResultsTypeNames.MULTIPLE_CHOICE:
return MultiChoiceElection.checkVote(vote, this.voteType);
return MultiChoiceElection.checkVote(vote, this.voteType, this.resultsType.properties);
case ElectionResultsTypeNames.APPROVAL:
return ApprovalElection.checkVote(vote, this.voteType);
case ElectionResultsTypeNames.BUDGET:
Expand Down
49 changes: 29 additions & 20 deletions src/types/metadata/election.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,42 +47,51 @@ export enum ElectionResultsTypeNames {
QUADRATIC = 'quadratic',
}

export type AbstainProperties = {
canAbstain: boolean;
abstainValues: Array<string>;
};

export type ChoiceProperties = {
repeatChoice: boolean;
numChoices: {
min: number;
max: number;
};
};

export type BudgetProperties = {
useCensusWeightAsBudget: boolean;
maxBudget: number;
minStep: number;
forceFullBudget: boolean;
};

export type ApprovalProperties = {
rejectValue: number;
acceptValue: number;
};

export type ElectionResultsType =
| {
name: ElectionResultsTypeNames.SINGLE_CHOICE_MULTIQUESTION;
properties: {};
}
| {
name: ElectionResultsTypeNames.MULTIPLE_CHOICE;
properties: {
canAbstain: boolean;
abstainValues: Array<string>;
repeatChoice: boolean;
};
properties: AbstainProperties & ChoiceProperties;
}
| {
name: ElectionResultsTypeNames.BUDGET;
properties: {
useCensusWeightAsBudget: boolean;
maxBudget: number;
minStep: number;
forceFullBudget: boolean;
};
properties: BudgetProperties;
}
| {
name: ElectionResultsTypeNames.APPROVAL;
properties: {
rejectValue: number;
acceptValue: number;
};
properties: ApprovalProperties;
}
| {
name: ElectionResultsTypeNames.QUADRATIC;
properties: {
useCensusWeightAsBudget: boolean;
maxBudget: number;
minStep: number;
forceFullBudget: boolean;
properties: BudgetProperties & {
quadraticCost: number;
};
};
Expand Down
14 changes: 13 additions & 1 deletion test/integration/election.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,7 @@ describe('Election integration tests', () => {
maxNumberOfChoices: 3,
canAbstain: true,
canRepeatChoices: false,
minNumberOfChoices: 2,
});

election.addQuestion('This is a title', 'This is a description', [
Expand Down Expand Up @@ -664,6 +665,10 @@ describe('Election integration tests', () => {
canAbstain: true,
repeatChoice: false,
abstainValues: ['5', '6', '7'],
numChoices: {
max: 3,
min: 2,
},
});
expect(election.results).toStrictEqual([
['5', '0', '0', '0', '0', '0', '0', '0'],
Expand All @@ -672,12 +677,19 @@ describe('Election integration tests', () => {
]);
expect(election.questions[0].numAbstains).toEqual('10');
expect(election.checkVote(new Vote([0, 5, 7]))).toBeUndefined();
expect(election.checkVote(new Vote([0, 1]))).toBeUndefined();
expect(() => {
election.checkVote(new Vote([0, 1]));
}).not.toThrow();
expect(() => {
election.checkVote(new Vote([5, 5, 7]));
}).toThrow('Choices are not unique');
expect(() => {
election.checkVote(new Vote([1]));
}).toThrow('Invalid number of choices, minimum is 2');
expect(() => {
election.checkVote(new Vote([0, 1, 5, 7]));
}).toThrow('Invalid number of choices');
}).toThrow('Invalid number of choices, maximum is 3');
expect(() => {
election.checkVote(new Vote([0, 15, 7]));
}).toThrow('Invalid choice value');
Expand Down

0 comments on commit 40b543b

Please sign in to comment.