OpenSlides/client/src/app/site/motions/services/motion-poll.service.ts
Maximilian Krambach d5101714d1 motion polls
2019-01-17 14:03:34 +01:00

182 lines
6.0 KiB
TypeScript

import { Injectable } from '@angular/core';
import { ConfigService } from 'app/core/services/config.service';
import { MotionPoll } from 'app/shared/models/motions/motion-poll';
import { PollService } from 'app/core/poll.service';
export type CalculablePollKey = 'votesvalid' | 'votesinvalid' | 'votescast' | 'yes' | 'no' | 'abstain';
/**
* Service class for motion polls.
*/
@Injectable({
providedIn: 'root'
})
export class MotionPollService extends PollService {
/**
* list of poll keys that are numbers and can be part of a quorum calculation
*/
public pollValues: CalculablePollKey[] = ['yes', 'no', 'abstain', 'votesvalid', 'votesinvalid', 'votescast'];
/**
* Constructor. Subscribes to the configuration values needed
* @param config ConfigService
*/
public constructor(config: ConfigService) {
super();
config.get('motions_poll_100_percent_base').subscribe(base => (this.percentBase = base));
config.get('motions_poll_default_majority_method').subscribe(method => (this.defaultMajorityMethod = method));
}
/**
* Calculates the percentage the given key reaches.
*
* @param poll
* @param key
* @returns a percentage number with two digits, null if the value cannot be calculated (consider 0 !== null)
*/
public calculatePercentage(poll: MotionPoll, key: CalculablePollKey): number {
const baseNumber = this.getBaseAmount(poll);
if (!baseNumber) {
return null;
}
switch (key) {
case 'abstain':
if (this.percentBase === 'YES_NO') {
return null;
}
break;
case 'votesinvalid':
if (this.percentBase !== 'CAST') {
return null;
}
break;
case 'votesvalid':
if (!['CAST', 'VALID'].includes(this.percentBase)) {
return null;
}
break;
case 'votescast':
if (this.percentBase !== 'CAST') {
return null;
}
}
return Math.round(((poll[key] * 100) / baseNumber) * 100) / 100;
}
/**
* Gets the number representing 100 percent for a given MotionPoll, depending
* on the configuration and the votes given.
*
* @param poll
* @returns the positive number representing 100 percent of the poll, 0 if
* the base cannot be calculated
*/
public getBaseAmount(poll: MotionPoll): number {
if (!poll) {
return 0;
}
switch (this.percentBase) {
case 'CAST':
if (!poll.votescast) {
return 0;
}
if (poll.votesinvalid < 0) {
return 0;
}
return poll.votescast;
case 'VALID':
if (poll.yes < 0 || poll.no < 0 || poll.abstain < 0) {
return 0;
}
return poll.votesvalid ? poll.votesvalid : 0;
case 'YES_NO_ABSTAIN':
if (poll.yes < 0 || poll.no < 0 || poll.abstain < 0) {
return 0;
}
return poll.yes + poll.no + poll.abstain;
case 'YES_NO':
if (poll.yes < 0 || poll.no < 0 || poll.abstain === -1) {
// It is not allowed to set 'Abstain' to 'majority' but exclude it from calculation.
// Setting 'Abstain' to 'undocumented' is possible, of course.
return 0;
}
return poll.yes + poll.no;
}
}
/**
* Calculates which number is needed for the quorum to be surpassed
* TODO: Methods still hard coded to mirror the server's.
*
* @param poll
* @param method (optional) majority calculation method. If none is given,
* the default as set in the config will be used.
* @returns the first integer number larger than the required majority,
* undefined if a quorum cannot be calculated.
*/
public calculateQuorum(poll: MotionPoll, method?: string): number {
if (!method) {
method = this.defaultMajorityMethod;
}
const baseNumber = this.getBaseAmount(poll);
if (!baseNumber) {
return undefined;
}
let result: number;
switch (method) {
case 'simple_majority':
result = baseNumber * 0.5;
break;
case 'two-thirds_majority':
result = (baseNumber / 3) * 2;
break;
case 'three-quarters_majority':
result = (baseNumber / 4) * 3;
break;
default:
return undefined;
}
// rounding up, or if a integer was hit, adding one.
if (result % 1 !== 0) {
return Math.ceil(result);
} else {
return result + 1;
}
}
/**
* Determines if a value is abstract (percentages cannot be calculated)
*
* @param poll
* @param value
* @returns true if the percentages should not be calculated
*/
public isAbstractValue(poll: MotionPoll, value: CalculablePollKey): boolean {
if (this.getBaseAmount(poll) === 0) {
return true;
}
switch (this.percentBase) {
case 'YES_NO':
if (['votescast', 'votesinvalid', 'votesvalid', 'abstain'].includes(value)) {
return true;
}
break;
case 'YES_NO_ABSTAIN':
if (['votescast', 'votesinvalid', 'votesvalid'].includes(value)) {
return true;
}
break;
case 'VALID':
if (['votesinvalid', 'votescast'].includes(value)) {
return true;
}
break;
}
if (poll[value] < 0) {
return true;
}
return false;
}
}