From 40b543b5f50850d4d8ca1433c78bbfc800de73d3 Mon Sep 17 00:00:00 2001 From: Marc Velmer Date: Thu, 31 Oct 2024 12:43:31 +0100 Subject: [PATCH] Added min/max number of choices in multichoice election --- src/types/election/multichoice.ts | 33 ++++++++++++++++++--- src/types/election/published.ts | 2 +- src/types/metadata/election.ts | 49 ++++++++++++++++++------------- test/integration/election.test.ts | 14 ++++++++- 4 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/types/election/multichoice.ts b/src/types/election/multichoice.ts index 7128adb5..7d2f2f77 100644 --- a/src/types/election/multichoice.ts +++ b/src/types/election/multichoice.ts @@ -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; } @@ -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 @@ -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; } @@ -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) => { @@ -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; } diff --git a/src/types/election/published.ts b/src/types/election/published.ts index 33be8460..91de017b 100644 --- a/src/types/election/published.ts +++ b/src/types/election/published.ts @@ -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: diff --git a/src/types/metadata/election.ts b/src/types/metadata/election.ts index 4b983e20..d5277cf0 100644 --- a/src/types/metadata/election.ts +++ b/src/types/metadata/election.ts @@ -47,6 +47,31 @@ export enum ElectionResultsTypeNames { QUADRATIC = 'quadratic', } +export type AbstainProperties = { + canAbstain: boolean; + abstainValues: Array; +}; + +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; @@ -54,35 +79,19 @@ export type ElectionResultsType = } | { name: ElectionResultsTypeNames.MULTIPLE_CHOICE; - properties: { - canAbstain: boolean; - abstainValues: Array; - 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; }; }; diff --git a/test/integration/election.test.ts b/test/integration/election.test.ts index d10f30e6..bf74726a 100644 --- a/test/integration/election.test.ts +++ b/test/integration/election.test.ts @@ -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', [ @@ -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'], @@ -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');