mirror of
https://github.com/ArenMg/aren.git
synced 2024-11-13 04:00:53 +00:00
Vote majoritaire
This commit is contained in:
parent
2d1eac7073
commit
d7c130d740
19 changed files with 995 additions and 0 deletions
72
src/main/java/fr/mieuxvoter/mj/CollectedTally.java
Normal file
72
src/main/java/fr/mieuxvoter/mj/CollectedTally.java
Normal file
|
@ -0,0 +1,72 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class CollectedTally implements TallyInterface {
|
||||
|
||||
Integer amountOfProposals = 0;
|
||||
Integer amountOfGrades = 0;
|
||||
|
||||
ProposalTally[] proposalsTallies;
|
||||
|
||||
public CollectedTally(Integer amountOfProposals, Integer amountOfGrades) {
|
||||
setAmountOfProposals(amountOfProposals);
|
||||
setAmountOfGrades(amountOfGrades);
|
||||
|
||||
proposalsTallies = new ProposalTally[amountOfProposals];
|
||||
for (int i = 0; i < amountOfProposals; i++) {
|
||||
ProposalTally proposalTally = new ProposalTally();
|
||||
Integer[] tally = new Integer[amountOfGrades];
|
||||
for (int j = 0; j < amountOfGrades; j++) {
|
||||
tally[j] = 0;
|
||||
}
|
||||
proposalTally.setTally(tally);
|
||||
proposalsTallies[i] = proposalTally;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProposalTallyInterface[] getProposalsTallies() {
|
||||
return proposalsTallies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getAmountOfJudges() {
|
||||
return guessAmountOfJudges();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getAmountOfProposals() {
|
||||
return this.amountOfProposals;
|
||||
}
|
||||
|
||||
public void setAmountOfProposals(Integer amountOfProposals) {
|
||||
this.amountOfProposals = amountOfProposals;
|
||||
}
|
||||
|
||||
public Integer getAmountOfGrades() {
|
||||
return amountOfGrades;
|
||||
}
|
||||
|
||||
public void setAmountOfGrades(Integer amountOfGrades) {
|
||||
this.amountOfGrades = amountOfGrades;
|
||||
}
|
||||
|
||||
protected BigInteger guessAmountOfJudges() {
|
||||
BigInteger amountOfJudges = BigInteger.ZERO;
|
||||
for (ProposalTallyInterface proposalTally : getProposalsTallies()) {
|
||||
amountOfJudges = proposalTally.getAmountOfJudgments().max(amountOfJudges);
|
||||
}
|
||||
return amountOfJudges;
|
||||
}
|
||||
|
||||
public void collect(Integer proposal, Integer grade) {
|
||||
assert (0 <= proposal);
|
||||
assert (amountOfProposals > proposal);
|
||||
assert (0 <= grade);
|
||||
assert (amountOfGrades > grade);
|
||||
|
||||
BigInteger[] tally = proposalsTallies[proposal].getTally();
|
||||
tally[grade] = tally[grade].add(BigInteger.ONE);
|
||||
}
|
||||
}
|
47
src/main/java/fr/mieuxvoter/mj/DefaultGradeTally.java
Normal file
47
src/main/java/fr/mieuxvoter/mj/DefaultGradeTally.java
Normal file
|
@ -0,0 +1,47 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Fill the missing judgments into the grade defined by `getDefaultGrade()`. This is an abstract
|
||||
* class to dry code between static default grade and median default grade.
|
||||
*/
|
||||
public abstract class DefaultGradeTally extends Tally implements TallyInterface {
|
||||
|
||||
/** Override this to choose the default grade for a given proposal. */
|
||||
protected abstract Integer getDefaultGradeForProposal(ProposalTallyInterface proposalTally);
|
||||
|
||||
// <domi41> /me is confused with why we need constructors in an abstract class?
|
||||
|
||||
public DefaultGradeTally(TallyInterface tally) {
|
||||
super(tally.getProposalsTallies(), tally.getAmountOfJudges());
|
||||
}
|
||||
|
||||
public DefaultGradeTally(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges) {
|
||||
super(proposalsTallies, amountOfJudges);
|
||||
}
|
||||
|
||||
public DefaultGradeTally(ProposalTallyInterface[] proposalsTallies, Long amountOfJudges) {
|
||||
super(proposalsTallies, amountOfJudges);
|
||||
}
|
||||
|
||||
public DefaultGradeTally(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges) {
|
||||
super(proposalsTallies, amountOfJudges);
|
||||
}
|
||||
|
||||
protected void fillWithDefaultGrade() {
|
||||
int amountOfProposals = getAmountOfProposals();
|
||||
for (int i = 0; i < amountOfProposals; i++) {
|
||||
ProposalTallyInterface proposalTally = getProposalsTallies()[i];
|
||||
Integer defaultGrade = getDefaultGradeForProposal(proposalTally);
|
||||
BigInteger amountOfJudgments = proposalTally.getAmountOfJudgments();
|
||||
BigInteger missingAmount = this.amountOfJudges.subtract(amountOfJudgments);
|
||||
int missingSign = missingAmount.compareTo(BigInteger.ZERO);
|
||||
assert (0 <= missingSign); // ERROR: More judgments than judges!
|
||||
if (0 < missingSign) {
|
||||
BigInteger[] rawTally = proposalTally.getTally();
|
||||
rawTally[defaultGrade] = rawTally[defaultGrade].add(missingAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
src/main/java/fr/mieuxvoter/mj/DeliberatorInterface.java
Normal file
18
src/main/java/fr/mieuxvoter/mj/DeliberatorInterface.java
Normal file
|
@ -0,0 +1,18 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
/**
|
||||
* A Deliberator takes in a poll's Tally, which holds the amount of judgments of each grade received
|
||||
* by each Proposal, and outputs that poll's Result, that is the final rank of each Proposal.
|
||||
*
|
||||
* <p>Ranks start at 1 ("best"), and increment towards "worst". Two proposal may share the same
|
||||
* rank, in extreme equality cases.
|
||||
*
|
||||
* <p>This is the main API of this library.
|
||||
*
|
||||
* <p>See MajorityJudgmentDeliberator for an implementation. One could implement other deliberators,
|
||||
* such as: - CentralJudgmentDeliberator - UsualJudgmentDeliberator
|
||||
*/
|
||||
public interface DeliberatorInterface {
|
||||
|
||||
public ResultInterface deliberate(TallyInterface tally) throws InvalidTallyException;
|
||||
}
|
13
src/main/java/fr/mieuxvoter/mj/IncoherentTallyException.java
Normal file
13
src/main/java/fr/mieuxvoter/mj/IncoherentTallyException.java
Normal file
|
@ -0,0 +1,13 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
/** Raised when the provided tally holds negative values, or infinity. */
|
||||
class IncoherentTallyException extends InvalidTallyException {
|
||||
|
||||
private static final long serialVersionUID = 5858986651601202903L;
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return ("The provided tally holds negative values, or infinity. "
|
||||
+ (null == super.getMessage() ? "" : super.getMessage()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
|
||||
/** Raised when the provided tally is invalid. */
|
||||
class InvalidTallyException extends InvalidParameterException {
|
||||
|
||||
private static final long serialVersionUID = 3033391835216704620L;
|
||||
}
|
197
src/main/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberator.java
Normal file
197
src/main/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberator.java
Normal file
|
@ -0,0 +1,197 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
/**
|
||||
* Deliberate using Majority Judgment.
|
||||
*
|
||||
* <p>Sorts Proposals by their median Grade. When two proposals share the same median Grade, give
|
||||
* reason to the largest group of people that did not give the median Grade.
|
||||
*
|
||||
* <p>This algorithm is score-based, for performance (and possible parallelization). Each Proposal
|
||||
* gets a score, higher (lexicographically) is "better" (depends of the meaning of the Grades). We
|
||||
* use Strings instead of Integers or raw Bits for the score. Improve if you feel like it and can
|
||||
* benchmark things.
|
||||
*
|
||||
* <p>https://en.wikipedia.org/wiki/Majority_judgment
|
||||
* https://fr.wikipedia.org/wiki/Jugement_majoritaire
|
||||
*
|
||||
* <p>Should this class be `final`?
|
||||
*/
|
||||
public final class MajorityJudgmentDeliberator implements DeliberatorInterface {
|
||||
|
||||
protected boolean favorContestation = true;
|
||||
protected boolean numerizeScore = false;
|
||||
|
||||
public MajorityJudgmentDeliberator() {}
|
||||
|
||||
public MajorityJudgmentDeliberator(boolean favorContestation) {
|
||||
this.favorContestation = favorContestation;
|
||||
}
|
||||
|
||||
public MajorityJudgmentDeliberator(boolean favorContestation, boolean numerizeScore) {
|
||||
this.favorContestation = favorContestation;
|
||||
this.numerizeScore = numerizeScore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultInterface deliberate(TallyInterface tally) throws InvalidTallyException {
|
||||
checkTally(tally);
|
||||
|
||||
ProposalTallyInterface[] tallies = tally.getProposalsTallies();
|
||||
BigInteger amountOfJudges = tally.getAmountOfJudges();
|
||||
Integer amountOfProposals = tally.getAmountOfProposals();
|
||||
|
||||
Result result = new Result();
|
||||
ProposalResult[] proposalResults = new ProposalResult[amountOfProposals];
|
||||
|
||||
// I. Compute the scores of each Proposal
|
||||
for (int proposalIndex = 0; proposalIndex < amountOfProposals; proposalIndex++) {
|
||||
ProposalTallyInterface proposalTally = tallies[proposalIndex];
|
||||
String score = computeScore(proposalTally, amountOfJudges);
|
||||
ProposalTallyAnalysis analysis =
|
||||
new ProposalTallyAnalysis(proposalTally, this.favorContestation);
|
||||
ProposalResult proposalResult = new ProposalResult();
|
||||
proposalResult.setScore(score);
|
||||
proposalResult.setAnalysis(analysis);
|
||||
// proposalResult.setRank(???); // rank is computed below, AFTER the score pass
|
||||
proposalResults[proposalIndex] = proposalResult;
|
||||
}
|
||||
|
||||
// II. Sort Proposals by score (lexicographical inverse)
|
||||
ProposalResult[] proposalResultsSorted = proposalResults.clone();
|
||||
assert (proposalResultsSorted[0].hashCode()
|
||||
== proposalResults[0].hashCode()); // we need a shallow clone
|
||||
Arrays.sort(
|
||||
proposalResultsSorted,
|
||||
(Comparator<ProposalResultInterface>) (p0, p1) -> p1.getScore().compareTo(p0.getScore()));
|
||||
|
||||
// III. Attribute a rank to each Proposal
|
||||
Integer rank = 1;
|
||||
for (int proposalIndex = 0; proposalIndex < amountOfProposals; proposalIndex++) {
|
||||
ProposalResult proposalResult = proposalResultsSorted[proposalIndex];
|
||||
Integer actualRank = rank;
|
||||
if (proposalIndex > 0) {
|
||||
ProposalResult proposalResultBefore = proposalResultsSorted[proposalIndex - 1];
|
||||
if (proposalResult.getScore().contentEquals(proposalResultBefore.getScore())) {
|
||||
actualRank = proposalResultBefore.getRank();
|
||||
}
|
||||
}
|
||||
proposalResult.setRank(actualRank);
|
||||
rank += 1;
|
||||
}
|
||||
|
||||
result.setProposalResults(proposalResults);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected void checkTally(TallyInterface tally) throws UnbalancedTallyException {
|
||||
if (!isTallyCoherent(tally)) {
|
||||
throw new IncoherentTallyException();
|
||||
}
|
||||
if (!isTallyBalanced(tally)) {
|
||||
throw new UnbalancedTallyException();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isTallyCoherent(TallyInterface tally) {
|
||||
boolean coherent = true;
|
||||
for (ProposalTallyInterface proposalTally : tally.getProposalsTallies()) {
|
||||
for (BigInteger gradeTally : proposalTally.getTally()) {
|
||||
if (-1 == gradeTally.compareTo(BigInteger.ZERO)) {
|
||||
coherent = false; // negative tallies are not coherent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return coherent;
|
||||
}
|
||||
|
||||
protected boolean isTallyBalanced(TallyInterface tally) {
|
||||
boolean balanced = true;
|
||||
BigInteger amountOfJudges = BigInteger.ZERO;
|
||||
boolean firstProposal = true;
|
||||
for (ProposalTallyInterface proposalTally : tally.getProposalsTallies()) {
|
||||
if (firstProposal) {
|
||||
amountOfJudges = proposalTally.getAmountOfJudgments();
|
||||
firstProposal = false;
|
||||
} else {
|
||||
if (0 != amountOfJudges.compareTo(proposalTally.getAmountOfJudgments())) {
|
||||
balanced = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return balanced;
|
||||
}
|
||||
|
||||
/** @see computeScore() below */
|
||||
protected String computeScore(ProposalTallyInterface tally, BigInteger amountOfJudges) {
|
||||
return computeScore(tally, amountOfJudges, this.favorContestation, this.numerizeScore);
|
||||
}
|
||||
|
||||
/**
|
||||
* A higher score means a better rank. Assumes that grades' tallies are provided from "worst"
|
||||
* grade to "best" grade.
|
||||
*
|
||||
* @param tally Holds the tallies of each Grade for a single Proposal
|
||||
* @param amountOfJudges
|
||||
* @param favorContestation Use the lower median, for example
|
||||
* @param onlyNumbers Do not use separation characters, match `^[0-9]+$`
|
||||
* @return the score of the proposal
|
||||
*/
|
||||
protected String computeScore(
|
||||
ProposalTallyInterface tally,
|
||||
BigInteger amountOfJudges,
|
||||
Boolean favorContestation,
|
||||
Boolean onlyNumbers) {
|
||||
ProposalTallyAnalysis analysis = new ProposalTallyAnalysis();
|
||||
int amountOfGrades = tally.getTally().length;
|
||||
int digitsForGrade = countDigits(amountOfGrades);
|
||||
int digitsForGroup = countDigits(amountOfJudges) + 1;
|
||||
|
||||
ProposalTallyInterface currentTally = tally.duplicate();
|
||||
|
||||
String score = "";
|
||||
for (int i = 0; i < amountOfGrades; i++) {
|
||||
|
||||
analysis.reanalyze(currentTally, favorContestation);
|
||||
|
||||
if (0 < i && !onlyNumbers) {
|
||||
score += "/";
|
||||
}
|
||||
|
||||
score += String.format("%0" + digitsForGrade + "d", analysis.getMedianGrade());
|
||||
|
||||
if (!onlyNumbers) {
|
||||
score += "_";
|
||||
}
|
||||
|
||||
score +=
|
||||
String.format(
|
||||
"%0" + digitsForGroup + "d",
|
||||
// We offset by amountOfJudges to keep a lexicographical order (no
|
||||
// negatives)
|
||||
// amountOfJudges + secondMedianGroupSize * secondMedianGroupSign
|
||||
amountOfJudges.add(
|
||||
analysis.getSecondMedianGroupSize()
|
||||
.multiply(
|
||||
BigInteger.valueOf(
|
||||
analysis.getSecondMedianGroupSign()))));
|
||||
|
||||
currentTally.moveJudgments(analysis.getMedianGrade(), analysis.getSecondMedianGrade());
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
protected int countDigits(int number) {
|
||||
return ("" + number).length();
|
||||
}
|
||||
|
||||
protected int countDigits(BigInteger number) {
|
||||
return ("" + number).length();
|
||||
}
|
||||
}
|
37
src/main/java/fr/mieuxvoter/mj/MedianDefaultTally.java
Normal file
37
src/main/java/fr/mieuxvoter/mj/MedianDefaultTally.java
Normal file
|
@ -0,0 +1,37 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Fill the missing judgments into the median grade of each proposal. Useful when the proposals have
|
||||
* not received the exact same amount of votes and the median grade is considered a sane default.
|
||||
*/
|
||||
public class MedianDefaultTally extends DefaultGradeTally implements TallyInterface {
|
||||
|
||||
public MedianDefaultTally(TallyInterface tally) {
|
||||
super(tally.getProposalsTallies(), tally.getAmountOfJudges());
|
||||
fillWithDefaultGrade();
|
||||
}
|
||||
|
||||
public MedianDefaultTally(
|
||||
ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges) {
|
||||
super(proposalsTallies, amountOfJudges);
|
||||
fillWithDefaultGrade();
|
||||
}
|
||||
|
||||
public MedianDefaultTally(ProposalTallyInterface[] proposalsTallies, Long amountOfJudges) {
|
||||
super(proposalsTallies, amountOfJudges);
|
||||
fillWithDefaultGrade();
|
||||
}
|
||||
|
||||
public MedianDefaultTally(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges) {
|
||||
super(proposalsTallies, amountOfJudges);
|
||||
fillWithDefaultGrade();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer getDefaultGradeForProposal(ProposalTallyInterface proposalTally) {
|
||||
ProposalTallyAnalysis analysis = new ProposalTallyAnalysis(proposalTally);
|
||||
return analysis.getMedianGrade();
|
||||
}
|
||||
}
|
75
src/main/java/fr/mieuxvoter/mj/NormalizedTally.java
Normal file
75
src/main/java/fr/mieuxvoter/mj/NormalizedTally.java
Normal file
|
@ -0,0 +1,75 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidParameterException;
|
||||
|
||||
/**
|
||||
* The deliberator expects the proposals' tallies to hold the same amount of judgments. This
|
||||
* NormalizedTally accepts tallies with disparate amounts of judgments per proposal, and normalizes
|
||||
* them to their least common multiple, which amounts to using percentages, except we don't use
|
||||
* floating-point arithmetic.
|
||||
*
|
||||
* <p>This is useful when there are too many proposals for judges to be expected to judge them all,
|
||||
* and all the proposals received reasonably similar amounts of judgments.
|
||||
*/
|
||||
public class NormalizedTally extends Tally implements TallyInterface {
|
||||
|
||||
public NormalizedTally(ProposalTallyInterface[] proposalsTallies) {
|
||||
super(proposalsTallies);
|
||||
initializeFromProposalsTallies(proposalsTallies);
|
||||
}
|
||||
|
||||
public NormalizedTally(TallyInterface tally) {
|
||||
super(tally.getProposalsTallies());
|
||||
initializeFromProposalsTallies(tally.getProposalsTallies());
|
||||
}
|
||||
|
||||
protected void initializeFromProposalsTallies(ProposalTallyInterface[] proposalsTallies) {
|
||||
Integer amountOfProposals = getAmountOfProposals();
|
||||
|
||||
// Compute the Least Common Multiple
|
||||
BigInteger amountOfJudges = BigInteger.ONE;
|
||||
for (ProposalTallyInterface proposalTally : proposalsTallies) {
|
||||
amountOfJudges = lcm(amountOfJudges, proposalTally.getAmountOfJudgments());
|
||||
}
|
||||
|
||||
if (0 == amountOfJudges.compareTo(BigInteger.ZERO)) {
|
||||
throw new InvalidParameterException(
|
||||
"Cannot normalize: one or more proposals have no judgments.");
|
||||
}
|
||||
|
||||
// Normalize proposals to the LCM
|
||||
ProposalTally[] normalizedTallies = new ProposalTally[amountOfProposals];
|
||||
for (int i = 0; i < amountOfProposals; i++) {
|
||||
ProposalTallyInterface proposalTally = proposalsTallies[i];
|
||||
ProposalTally normalizedTally = new ProposalTally(proposalTally);
|
||||
BigInteger factor = amountOfJudges.divide(proposalTally.getAmountOfJudgments());
|
||||
Integer amountOfGrades = proposalTally.getTally().length;
|
||||
BigInteger[] gradesTallies = normalizedTally.getTally();
|
||||
for (int j = 0; j < amountOfGrades; j++) {
|
||||
gradesTallies[j] = gradesTallies[j].multiply(factor);
|
||||
}
|
||||
normalizedTallies[i] = normalizedTally;
|
||||
}
|
||||
|
||||
setProposalsTallies(normalizedTallies);
|
||||
setAmountOfJudges(amountOfJudges);
|
||||
}
|
||||
|
||||
/**
|
||||
* Least Common Multiple
|
||||
*
|
||||
* <p>http://en.wikipedia.org/wiki/Least_common_multiple
|
||||
*
|
||||
* <p>lcm( 6, 9 ) = 18 lcm( 4, 9 ) = 36 lcm( 0, 9 ) = 0 lcm( 0, 0 ) = 0
|
||||
*
|
||||
* @author www.java2s.com
|
||||
* @param a first integer
|
||||
* @param b second integer
|
||||
* @return least common multiple of a and b
|
||||
*/
|
||||
public static BigInteger lcm(BigInteger a, BigInteger b) {
|
||||
if (a.signum() == 0 || b.signum() == 0) return BigInteger.ZERO;
|
||||
return a.divide(a.gcd(b)).multiply(b).abs();
|
||||
}
|
||||
}
|
34
src/main/java/fr/mieuxvoter/mj/ProposalResult.java
Normal file
34
src/main/java/fr/mieuxvoter/mj/ProposalResult.java
Normal file
|
@ -0,0 +1,34 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
public class ProposalResult implements ProposalResultInterface {
|
||||
|
||||
protected Integer rank;
|
||||
|
||||
protected String score;
|
||||
|
||||
protected ProposalTallyAnalysis analysis;
|
||||
|
||||
public Integer getRank() {
|
||||
return rank;
|
||||
}
|
||||
|
||||
public void setRank(Integer rank) {
|
||||
this.rank = rank;
|
||||
}
|
||||
|
||||
public String getScore() {
|
||||
return score;
|
||||
}
|
||||
|
||||
public void setScore(String score) {
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
public ProposalTallyAnalysis getAnalysis() {
|
||||
return analysis;
|
||||
}
|
||||
|
||||
public void setAnalysis(ProposalTallyAnalysis analysis) {
|
||||
this.analysis = analysis;
|
||||
}
|
||||
}
|
21
src/main/java/fr/mieuxvoter/mj/ProposalResultInterface.java
Normal file
21
src/main/java/fr/mieuxvoter/mj/ProposalResultInterface.java
Normal file
|
@ -0,0 +1,21 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
public interface ProposalResultInterface {
|
||||
|
||||
/**
|
||||
* Rank starts at 1 ("best" proposal), and goes upwards. Multiple Proposals may receive the same
|
||||
* rank, in the extreme case where they received the exact same judgments, or the same judgment
|
||||
* repartition in normalized tallies.
|
||||
*/
|
||||
public Integer getRank();
|
||||
|
||||
/**
|
||||
* This score was used to compute the rank. It is made of integer characters, with zeroes for
|
||||
* padding. Inverse lexicographical order: "higher" is "better". You're probably never going to
|
||||
* need this, but it's here anyway.
|
||||
*/
|
||||
public String getScore();
|
||||
|
||||
/** Get more data about the proposal tally, such as the median grade. */
|
||||
public ProposalTallyAnalysis getAnalysis();
|
||||
}
|
92
src/main/java/fr/mieuxvoter/mj/ProposalTally.java
Normal file
92
src/main/java/fr/mieuxvoter/mj/ProposalTally.java
Normal file
|
@ -0,0 +1,92 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ProposalTally implements ProposalTallyInterface {
|
||||
|
||||
/**
|
||||
* Amounts of judgments received per grade, from "worst" grade to "best" grade. Those are
|
||||
* BigIntegers because of our LCM-based normalization shenanigans.
|
||||
*/
|
||||
protected BigInteger[] tally;
|
||||
|
||||
public ProposalTally() {}
|
||||
|
||||
public ProposalTally(String[] tally) {
|
||||
setTally(tally);
|
||||
}
|
||||
|
||||
public ProposalTally(Integer[] tally) {
|
||||
setTally(tally);
|
||||
}
|
||||
|
||||
public ProposalTally(Long[] tally) {
|
||||
setTally(tally);
|
||||
}
|
||||
|
||||
public ProposalTally(BigInteger[] tally) {
|
||||
setTally(tally);
|
||||
}
|
||||
|
||||
public ProposalTally(ProposalTallyInterface proposalTally) {
|
||||
setTally(Arrays.copyOf(proposalTally.getTally(), proposalTally.getTally().length));
|
||||
}
|
||||
|
||||
public void setTally(String[] tally) {
|
||||
int tallyLength = tally.length;
|
||||
BigInteger[] bigTally = new BigInteger[tallyLength];
|
||||
for (int i = 0; i < tallyLength; i++) {
|
||||
bigTally[i] = new BigInteger(tally[i]);
|
||||
}
|
||||
setTally(bigTally);
|
||||
}
|
||||
|
||||
public void setTally(Integer[] tally) {
|
||||
int tallyLength = tally.length;
|
||||
BigInteger[] bigTally = new BigInteger[tallyLength];
|
||||
for (int i = 0; i < tallyLength; i++) {
|
||||
bigTally[i] = BigInteger.valueOf(tally[i]);
|
||||
}
|
||||
setTally(bigTally);
|
||||
}
|
||||
|
||||
public void setTally(Long[] tally) {
|
||||
int tallyLength = tally.length;
|
||||
BigInteger[] bigTally = new BigInteger[tallyLength];
|
||||
for (int i = 0; i < tallyLength; i++) {
|
||||
bigTally[i] = BigInteger.valueOf(tally[i]);
|
||||
}
|
||||
setTally(bigTally);
|
||||
}
|
||||
|
||||
public void setTally(BigInteger[] tally) {
|
||||
this.tally = tally;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger[] getTally() {
|
||||
return this.tally;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProposalTallyInterface duplicate() {
|
||||
return new ProposalTally(Arrays.copyOf(this.tally, this.tally.length));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveJudgments(Integer fromGrade, Integer intoGrade) {
|
||||
this.tally[intoGrade] = this.tally[intoGrade].add(this.tally[fromGrade]);
|
||||
this.tally[fromGrade] = BigInteger.ZERO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getAmountOfJudgments() {
|
||||
BigInteger sum = BigInteger.ZERO;
|
||||
int tallyLength = this.tally.length;
|
||||
for (int i = 0; i < tallyLength; i++) {
|
||||
sum = sum.add(this.tally[i]);
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
}
|
179
src/main/java/fr/mieuxvoter/mj/ProposalTallyAnalysis.java
Normal file
179
src/main/java/fr/mieuxvoter/mj/ProposalTallyAnalysis.java
Normal file
|
@ -0,0 +1,179 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Collect useful data on a proposal tally. Does NOT compute the rank, but provides all we need.
|
||||
*
|
||||
* <p>This uses BigInteger because in a normalization scenario we use the smallest common multiple
|
||||
* of the amounts of judges of proposals. It makes the code harder to read and understand, but it
|
||||
* allows us to bypass the floating-point nightmare of the normalization of merit profiles, which is
|
||||
* one way to handle default grades on some polls.
|
||||
*/
|
||||
public class ProposalTallyAnalysis {
|
||||
|
||||
protected ProposalTallyInterface tally;
|
||||
|
||||
protected BigInteger totalSize = BigInteger.ZERO; // amount of judges
|
||||
|
||||
protected Integer medianGrade = 0;
|
||||
|
||||
protected BigInteger medianGroupSize = BigInteger.ZERO; // amount of judges in the median group
|
||||
|
||||
protected Integer contestationGrade = 0; // "best" grade of the contestation group
|
||||
|
||||
protected BigInteger contestationGroupSize = BigInteger.ZERO; // of lower grades than median
|
||||
|
||||
protected Integer adhesionGrade = 0; // "worst" grade of the adhesion group
|
||||
|
||||
protected BigInteger adhesionGroupSize = BigInteger.ZERO; // of higher grades than median
|
||||
|
||||
protected Integer secondMedianGrade = 0; // grade of the biggest group out of the median
|
||||
|
||||
protected BigInteger secondMedianGroupSize = BigInteger.ZERO; // either contestation or adhesion
|
||||
|
||||
protected Integer secondMedianGroupSign =
|
||||
0; // -1 for contestation, +1 for adhesion, 0 for empty group size
|
||||
|
||||
public ProposalTallyAnalysis() {}
|
||||
|
||||
public ProposalTallyAnalysis(ProposalTallyInterface tally) {
|
||||
reanalyze(tally);
|
||||
}
|
||||
|
||||
public ProposalTallyAnalysis(ProposalTallyInterface tally, Boolean favorContestation) {
|
||||
reanalyze(tally, favorContestation);
|
||||
}
|
||||
|
||||
public void reanalyze(ProposalTallyInterface tally) {
|
||||
reanalyze(tally, true);
|
||||
}
|
||||
|
||||
public void reanalyze(ProposalTallyInterface tally, Boolean favorContestation) {
|
||||
this.tally = tally;
|
||||
this.totalSize = BigInteger.ZERO;
|
||||
this.medianGrade = 0;
|
||||
this.medianGroupSize = BigInteger.ZERO;
|
||||
this.contestationGrade = 0;
|
||||
this.contestationGroupSize = BigInteger.ZERO;
|
||||
this.adhesionGrade = 0;
|
||||
this.adhesionGroupSize = BigInteger.ZERO;
|
||||
this.secondMedianGrade = 0;
|
||||
this.secondMedianGroupSize = BigInteger.ZERO;
|
||||
this.secondMedianGroupSign = 0;
|
||||
|
||||
BigInteger[] gradesTallies = this.tally.getTally();
|
||||
int amountOfGrades = gradesTallies.length;
|
||||
|
||||
for (int grade = 0; grade < amountOfGrades; grade++) {
|
||||
BigInteger gradeTally = gradesTallies[grade];
|
||||
// assert(0 <= gradeTally); // Negative tallies are not allowed.
|
||||
this.totalSize = this.totalSize.add(gradeTally);
|
||||
}
|
||||
|
||||
Integer medianOffset = 1;
|
||||
if (!favorContestation) {
|
||||
medianOffset = 2;
|
||||
}
|
||||
BigInteger medianCursor =
|
||||
this.totalSize.add(BigInteger.valueOf(medianOffset)).divide(BigInteger.valueOf(2));
|
||||
// Long medianCursor = (long) Math.floor((this.totalSize + medianOffset) / 2.0);
|
||||
|
||||
BigInteger tallyBeforeCursor = BigInteger.ZERO;
|
||||
BigInteger tallyCursor = BigInteger.ZERO;
|
||||
Boolean foundMedian = false;
|
||||
Integer contestationGrade = 0;
|
||||
Integer adhesionGrade = 0;
|
||||
for (int grade = 0; grade < amountOfGrades; grade++) {
|
||||
BigInteger gradeTally = gradesTallies[grade];
|
||||
tallyBeforeCursor = tallyCursor;
|
||||
tallyCursor = tallyCursor.add(gradeTally);
|
||||
|
||||
if (!foundMedian) {
|
||||
if (-1 < tallyCursor.compareTo(medianCursor)) { // tallyCursor >= medianCursor
|
||||
foundMedian = true;
|
||||
this.medianGrade = grade;
|
||||
this.contestationGroupSize = tallyBeforeCursor;
|
||||
this.medianGroupSize = gradeTally;
|
||||
this.adhesionGroupSize =
|
||||
this.totalSize
|
||||
.subtract(this.contestationGroupSize)
|
||||
.subtract(this.medianGroupSize);
|
||||
} else {
|
||||
if (1 == gradeTally.compareTo(BigInteger.ZERO)) { // 0 < gradeTally
|
||||
contestationGrade = grade;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (1 == gradeTally.compareTo(BigInteger.ZERO) && 0 == adhesionGrade) {
|
||||
adhesionGrade = grade;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.contestationGrade = contestationGrade;
|
||||
this.adhesionGrade = adhesionGrade;
|
||||
this.secondMedianGroupSize = this.contestationGroupSize.max(this.adhesionGroupSize);
|
||||
this.secondMedianGroupSign = 0;
|
||||
// if (this.contestationGroupSize < this.adhesionGroupSize) {
|
||||
if (1 == this.adhesionGroupSize.compareTo(this.contestationGroupSize)) {
|
||||
this.secondMedianGrade = this.adhesionGrade;
|
||||
this.secondMedianGroupSign = 1;
|
||||
// } else if (this.contestationGroupSize > this.adhesionGroupSize) {
|
||||
} else if (1 == this.contestationGroupSize.compareTo(this.adhesionGroupSize)) {
|
||||
this.secondMedianGrade = this.contestationGrade;
|
||||
this.secondMedianGroupSign = -1;
|
||||
} else {
|
||||
if (favorContestation) {
|
||||
this.secondMedianGrade = this.contestationGrade;
|
||||
this.secondMedianGroupSign = -1;
|
||||
} else {
|
||||
this.secondMedianGrade = this.adhesionGrade;
|
||||
this.secondMedianGroupSign = 1;
|
||||
}
|
||||
}
|
||||
if (0 == this.secondMedianGroupSize.compareTo(BigInteger.ZERO)) {
|
||||
this.secondMedianGroupSign = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public BigInteger getTotalSize() {
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
public Integer getMedianGrade() {
|
||||
return medianGrade;
|
||||
}
|
||||
|
||||
public BigInteger getMedianGroupSize() {
|
||||
return medianGroupSize;
|
||||
}
|
||||
|
||||
public Integer getContestationGrade() {
|
||||
return contestationGrade;
|
||||
}
|
||||
|
||||
public BigInteger getContestationGroupSize() {
|
||||
return contestationGroupSize;
|
||||
}
|
||||
|
||||
public Integer getAdhesionGrade() {
|
||||
return adhesionGrade;
|
||||
}
|
||||
|
||||
public BigInteger getAdhesionGroupSize() {
|
||||
return adhesionGroupSize;
|
||||
}
|
||||
|
||||
public Integer getSecondMedianGrade() {
|
||||
return secondMedianGrade;
|
||||
}
|
||||
|
||||
public BigInteger getSecondMedianGroupSize() {
|
||||
return secondMedianGroupSize;
|
||||
}
|
||||
|
||||
public Integer getSecondMedianGroupSign() {
|
||||
return secondMedianGroupSign;
|
||||
}
|
||||
}
|
29
src/main/java/fr/mieuxvoter/mj/ProposalTallyInterface.java
Normal file
29
src/main/java/fr/mieuxvoter/mj/ProposalTallyInterface.java
Normal file
|
@ -0,0 +1,29 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Also known as the merit profile of a proposal (aka. candidate), this holds the amounts of
|
||||
* judgments received per grade.
|
||||
*/
|
||||
public interface ProposalTallyInterface {
|
||||
|
||||
/**
|
||||
* The tallies of each Grade, that is the amount of judgments received for each Grade by the
|
||||
* Proposal, from "worst" ("most conservative") Grade to "best" Grade.
|
||||
*/
|
||||
public BigInteger[] getTally();
|
||||
|
||||
/**
|
||||
* Should be the sum of getTally()
|
||||
*
|
||||
* @return The total amount of judgments received by this proposal.
|
||||
*/
|
||||
public BigInteger getAmountOfJudgments();
|
||||
|
||||
/** Homemade factory to skip the clone() shenanigans. Used by the score calculus. */
|
||||
public ProposalTallyInterface duplicate();
|
||||
|
||||
/** Move judgments that were fromGrade into intoGrade. Used by the score calculus. */
|
||||
public void moveJudgments(Integer fromGrade, Integer intoGrade);
|
||||
}
|
14
src/main/java/fr/mieuxvoter/mj/Result.java
Normal file
14
src/main/java/fr/mieuxvoter/mj/Result.java
Normal file
|
@ -0,0 +1,14 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
public class Result implements ResultInterface {
|
||||
|
||||
protected ProposalResultInterface[] proposalResults;
|
||||
|
||||
public ProposalResultInterface[] getProposalResults() {
|
||||
return proposalResults;
|
||||
}
|
||||
|
||||
public void setProposalResults(ProposalResultInterface[] proposalResults) {
|
||||
this.proposalResults = proposalResults;
|
||||
}
|
||||
}
|
12
src/main/java/fr/mieuxvoter/mj/ResultInterface.java
Normal file
12
src/main/java/fr/mieuxvoter/mj/ResultInterface.java
Normal file
|
@ -0,0 +1,12 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
public interface ResultInterface {
|
||||
|
||||
/**
|
||||
* ProposalResults are not ordered by rank, they are in the order the proposals' tallies were
|
||||
* submitted.
|
||||
*
|
||||
* @return an array of `ProposalResult`, in the order the `ProposalTally`s were submitted.
|
||||
*/
|
||||
public ProposalResultInterface[] getProposalResults();
|
||||
}
|
53
src/main/java/fr/mieuxvoter/mj/StaticDefaultTally.java
Normal file
53
src/main/java/fr/mieuxvoter/mj/StaticDefaultTally.java
Normal file
|
@ -0,0 +1,53 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class StaticDefaultTally extends DefaultGradeTally implements TallyInterface {
|
||||
|
||||
/**
|
||||
* Grades are represented as numbers, as indices in a list. Grades start from 0 ("worst" grade,
|
||||
* most conservative) and go upwards. Values out of the range of grades defined in the tally
|
||||
* will yield errors.
|
||||
*
|
||||
* <p>Example:
|
||||
*
|
||||
* <p>0 == REJECT 1 == PASSABLE 2 == GOOD 3 == EXCELLENT
|
||||
*/
|
||||
protected Integer defaultGrade = 0;
|
||||
|
||||
public StaticDefaultTally(TallyInterface tally, Integer defaultGrade) {
|
||||
super(tally.getProposalsTallies(), tally.getAmountOfJudges());
|
||||
this.defaultGrade = defaultGrade;
|
||||
fillWithDefaultGrade();
|
||||
}
|
||||
|
||||
public StaticDefaultTally(
|
||||
ProposalTallyInterface[] proposalsTallies,
|
||||
BigInteger amountOfJudges,
|
||||
Integer defaultGrade) {
|
||||
super(proposalsTallies, amountOfJudges);
|
||||
this.defaultGrade = defaultGrade;
|
||||
fillWithDefaultGrade();
|
||||
}
|
||||
|
||||
public StaticDefaultTally(
|
||||
ProposalTallyInterface[] proposalsTallies, Long amountOfJudges, Integer defaultGrade) {
|
||||
super(proposalsTallies, amountOfJudges);
|
||||
this.defaultGrade = defaultGrade;
|
||||
fillWithDefaultGrade();
|
||||
}
|
||||
|
||||
public StaticDefaultTally(
|
||||
ProposalTallyInterface[] proposalsTallies,
|
||||
Integer amountOfJudges,
|
||||
Integer defaultGrade) {
|
||||
super(proposalsTallies, amountOfJudges);
|
||||
this.defaultGrade = defaultGrade;
|
||||
fillWithDefaultGrade();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer getDefaultGradeForProposal(ProposalTallyInterface proposalTally) {
|
||||
return this.defaultGrade;
|
||||
}
|
||||
}
|
61
src/main/java/fr/mieuxvoter/mj/Tally.java
Normal file
61
src/main/java/fr/mieuxvoter/mj/Tally.java
Normal file
|
@ -0,0 +1,61 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* A Basic implementation of a TallyInterface that reads from an array of ProposalTallyInterface.
|
||||
*/
|
||||
public class Tally implements TallyInterface {
|
||||
|
||||
protected ProposalTallyInterface[] proposalsTallies;
|
||||
|
||||
protected BigInteger amountOfJudges = BigInteger.ZERO;
|
||||
|
||||
public Tally(ProposalTallyInterface[] proposalsTallies) {
|
||||
setProposalsTallies(proposalsTallies);
|
||||
guessAmountOfJudges();
|
||||
}
|
||||
|
||||
public Tally(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges) {
|
||||
setProposalsTallies(proposalsTallies);
|
||||
setAmountOfJudges(amountOfJudges);
|
||||
}
|
||||
|
||||
public Tally(ProposalTallyInterface[] proposalsTallies, Long amountOfJudges) {
|
||||
setProposalsTallies(proposalsTallies);
|
||||
setAmountOfJudges(BigInteger.valueOf(amountOfJudges));
|
||||
}
|
||||
|
||||
public Tally(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges) {
|
||||
setProposalsTallies(proposalsTallies);
|
||||
setAmountOfJudges(BigInteger.valueOf(amountOfJudges));
|
||||
}
|
||||
|
||||
public ProposalTallyInterface[] getProposalsTallies() {
|
||||
return proposalsTallies;
|
||||
}
|
||||
|
||||
public void setProposalsTallies(ProposalTallyInterface[] proposalsTallies) {
|
||||
this.proposalsTallies = proposalsTallies;
|
||||
}
|
||||
|
||||
public Integer getAmountOfProposals() {
|
||||
return proposalsTallies.length;
|
||||
}
|
||||
|
||||
public BigInteger getAmountOfJudges() {
|
||||
return amountOfJudges;
|
||||
}
|
||||
|
||||
public void setAmountOfJudges(BigInteger amountOfJudges) {
|
||||
this.amountOfJudges = amountOfJudges;
|
||||
}
|
||||
|
||||
protected void guessAmountOfJudges() {
|
||||
BigInteger amountOfJudges = BigInteger.ZERO;
|
||||
for (ProposalTallyInterface proposalTally : getProposalsTallies()) {
|
||||
amountOfJudges = proposalTally.getAmountOfJudgments().max(amountOfJudges);
|
||||
}
|
||||
setAmountOfJudges(amountOfJudges);
|
||||
}
|
||||
}
|
12
src/main/java/fr/mieuxvoter/mj/TallyInterface.java
Normal file
12
src/main/java/fr/mieuxvoter/mj/TallyInterface.java
Normal file
|
@ -0,0 +1,12 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public interface TallyInterface {
|
||||
|
||||
public ProposalTallyInterface[] getProposalsTallies();
|
||||
|
||||
public BigInteger getAmountOfJudges();
|
||||
|
||||
public Integer getAmountOfProposals();
|
||||
}
|
20
src/main/java/fr/mieuxvoter/mj/UnbalancedTallyException.java
Normal file
20
src/main/java/fr/mieuxvoter/mj/UnbalancedTallyException.java
Normal file
|
@ -0,0 +1,20 @@
|
|||
package fr.mieuxvoter.mj;
|
||||
|
||||
/**
|
||||
* Raised when the provided tally does not hold the same amount of judgments for each proposal, and
|
||||
* normalization is required.
|
||||
*/
|
||||
class UnbalancedTallyException extends InvalidTallyException {
|
||||
|
||||
private static final long serialVersionUID = 5041093000505081735L;
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return ("The provided tally is unbalanced, as some proposals received more judgments than"
|
||||
+ " others. \n"
|
||||
+ "You need to set a strategy for balancing tallies. To that effect, \n"
|
||||
+ "you may use StaticDefaultTally, MedianDefaultTally, or NormalizedTally"
|
||||
+ " instead of Tally. \n"
|
||||
+ (null == super.getMessage() ? "" : super.getMessage()));
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue