This should work (at least, the test-suite passes). This is our first java library, and we're newbies, so be kind and do patch what's gouging your eyes. This implementation is score-based, for performance. There are many other ways of implementing MJ. There is no support (yet) for a default grade, but you can do it yourself while building the tally. Special thanks to @plguhur for the assistance, and the whole MieuxVoter operational team. /spend 36hpull/8/head
commit
7d9b24927e
@ -0,0 +1,8 @@
|
|||||||
|
/.settings
|
||||||
|
/.classpath
|
||||||
|
/.project
|
||||||
|
/.gradle
|
||||||
|
|
||||||
|
/build
|
||||||
|
|
||||||
|
*.class
|
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 MieuxVoter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
@ -0,0 +1,77 @@
|
|||||||
|
# Majority Judgment Library for Java
|
||||||
|
|
||||||
|
Test-driven java library to help deliberate using Majority Judgment.
|
||||||
|
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
Collect the **tallies** for each Proposal (aka. Candidate) by your own means,
|
||||||
|
provide them to the `MajorityJudgmentDeliberator`, and get back the **rank** of each Proposal.
|
||||||
|
|
||||||
|
Let's say you have the following tally:
|
||||||
|
|
||||||
|
| | To Reject | Poor | Passable | Somewhat Good | Good | Very Good | Excellent |
|
||||||
|
|------------|-----------|------|----------|---------------|------|-----------|-----------|
|
||||||
|
| Proposal A | 4 | 5 | 2 | 1 | 3 | 1 | 2 |
|
||||||
|
| Proposal B | 3 | 6 | 2 | 2 | 2 | 1 | 2 |
|
||||||
|
| … | | | | | | | |
|
||||||
|
| | | | | | | | |
|
||||||
|
|
||||||
|
|
||||||
|
``` java
|
||||||
|
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
|
||||||
|
TallyInterface tally = new Tally(new ProposalTallyInterface[] {
|
||||||
|
// Amounts of judgments received for each grade, from "worst" grade to "best" grade
|
||||||
|
new ProposalTally(new Integer[]{4, 5, 2, 1, 3, 1, 2}), // Proposal A
|
||||||
|
new ProposalTally(new Integer[]{3, 6, 2, 1, 3, 1, 2}), // Proposal B
|
||||||
|
// …
|
||||||
|
});
|
||||||
|
ResultInterface result = mj.deliberate(tally);
|
||||||
|
|
||||||
|
// Each proposal result has a rank, and results are returned by input order
|
||||||
|
assert(2 == result.getProposalResults().length);
|
||||||
|
assert(2 == result.getProposalResults()[0].getRank()); // Proposal A
|
||||||
|
assert(1 == result.getProposalResults()[1].getRank()); // Proposal B
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Run the test-suite
|
||||||
|
|
||||||
|
`CTRL+F11` in Eclipse.
|
||||||
|
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
- [ ] Unit-Tests
|
||||||
|
- [ ] Deliberation algorithm
|
||||||
|
- [ ] Release v0.1.0
|
||||||
|
- [ ] Allow choosing a default grade
|
||||||
|
- [ ] Release v0.2.0
|
||||||
|
- [ ] Publish on package repositories
|
||||||
|
- [ ] Maven
|
||||||
|
- [ ] … ? (please share your knowledge to help us!)
|
||||||
|
- [ ] Release v0.3.0
|
||||||
|
- [ ] Use it somewhere in another app, adjust API as needed (one last time)
|
||||||
|
- [ ] Release v1.0.0
|
||||||
|
|
||||||
|
|
||||||
|
## Gondor calls for Help!
|
||||||
|
|
||||||
|
We are not accustomed to Java library development and we'd love reviews from seasoned veterans !
|
||||||
|
|
||||||
|
Feel free to fork and request merges for your contributions and active readings !
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](./LICENSE.md) → _Do whatever you want except complain._
|
||||||
|
|
||||||
|
Majority Judgment itself is part of the Commons, obviously.
|
||||||
|
|
||||||
|
|
||||||
|
## Fund us
|
||||||
|
|
||||||
|
We'd love to invest more energy in Majority Judgment development.
|
||||||
|
|
||||||
|
Please consider funding us, every bit helps : https://www.paypal.com/donate/?hosted_button_id=QD6U4D323WV4S
|
||||||
|
|
@ -0,0 +1,16 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Deliberator takes in a poll's Tally,
|
||||||
|
* that is the amount of grades received by each Proposal,
|
||||||
|
* and outputs the poll's Result,
|
||||||
|
* that is the final rank of each Proposal.
|
||||||
|
*
|
||||||
|
* This is the main API of this library.
|
||||||
|
*
|
||||||
|
* See MajorityJudgmentDeliberator for an implementation.
|
||||||
|
*/
|
||||||
|
public interface DeliberatorInterface {
|
||||||
|
public ResultInterface deliberate(TallyInterface tally);
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deliberate using Majority Judgment.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* https://en.wikipedia.org/wiki/Majority_judgment
|
||||||
|
* https://fr.wikipedia.org/wiki/Jugement_majoritaire
|
||||||
|
*
|
||||||
|
* Should this class be "final" ?
|
||||||
|
*/
|
||||||
|
public class MajorityJudgmentDeliberator implements DeliberatorInterface {
|
||||||
|
|
||||||
|
public ResultInterface deliberate(TallyInterface tally) {
|
||||||
|
ProposalTallyInterface[] tallies = tally.getProposalsTallies();
|
||||||
|
Long 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);
|
||||||
|
ProposalResult proposalResult = new ProposalResult();
|
||||||
|
proposalResult.setScore(score);
|
||||||
|
//proposalResult.setRank(???); // rank is computed below, AFTER the score pass
|
||||||
|
proposalResults[proposalIndex] = proposalResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// II. Sort Proposals by score
|
||||||
|
ProposalResult[] proposalResultsSorted = proposalResults.clone();
|
||||||
|
assert(proposalResultsSorted[0].hashCode() == proposalResults[0].hashCode()); // we need a shallow clone
|
||||||
|
Arrays.sort(proposalResultsSorted, new Comparator<ProposalResultInterface>() {
|
||||||
|
@Override
|
||||||
|
public int compare(ProposalResultInterface p0, ProposalResultInterface p1) {
|
||||||
|
return 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String computeScore(ProposalTallyInterface tally, Long amountOfJudges) {
|
||||||
|
return computeScore(tally, amountOfJudges, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String computeScore(
|
||||||
|
ProposalTallyInterface tally,
|
||||||
|
Long amountOfJudges,
|
||||||
|
Boolean favorContestation,
|
||||||
|
Boolean onlyNumbers
|
||||||
|
) {
|
||||||
|
ProposalTallyAnalysis analysis = new ProposalTallyAnalysis();
|
||||||
|
int amountOfGrades = tally.getTally().length;
|
||||||
|
int digitsForGrade = ("" + amountOfGrades).length();
|
||||||
|
int digitsForGroup = ("" + amountOfJudges).length();
|
||||||
|
|
||||||
|
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",
|
||||||
|
amountOfJudges + analysis.getSecondMedianGroupSize() * analysis.getSecondMedianGroupSign()
|
||||||
|
);
|
||||||
|
|
||||||
|
currentTally.moveJudgments(analysis.getMedianGrade(), analysis.getSecondMedianGrade());
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
|
||||||
|
public class ProposalResult implements ProposalResultInterface {
|
||||||
|
|
||||||
|
protected Integer rank;
|
||||||
|
|
||||||
|
protected String score;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.
|
||||||
|
*/
|
||||||
|
public Integer getRank();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This score was used to compute the rank.
|
||||||
|
* It is made of integer characters, with zeroes for padding.
|
||||||
|
* Reverse lexicographical order: "higher" is "better".
|
||||||
|
* You're probably never going to need this, but it's here anyway.
|
||||||
|
*/
|
||||||
|
public String getScore();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class ProposalTally implements ProposalTallyInterface {
|
||||||
|
|
||||||
|
protected Long[] tally;
|
||||||
|
|
||||||
|
// Should we allow this as well?
|
||||||
|
//public ProposalTally() {}
|
||||||
|
|
||||||
|
public ProposalTally(Integer[] tally) {
|
||||||
|
int tallyLength = tally.length;
|
||||||
|
Long[] doublesTally = new Long[tallyLength];
|
||||||
|
for (int i = 0 ; i < tallyLength ; i++) {
|
||||||
|
doublesTally[i] = Long.valueOf(tally[i]);
|
||||||
|
}
|
||||||
|
setTally(doublesTally);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProposalTally(Long[] tally) {
|
||||||
|
setTally(tally);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTally(Long[] tally) {
|
||||||
|
this.tally = tally;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long[] 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[fromGrade];
|
||||||
|
this.tally[fromGrade] = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect useful data on a proposal tally.
|
||||||
|
* Does NOT compute the rank, but provides all we need
|
||||||
|
*/
|
||||||
|
public class ProposalTallyAnalysis {
|
||||||
|
|
||||||
|
protected ProposalTallyInterface tally;
|
||||||
|
|
||||||
|
protected Long totalSize = 0L; // amount of judges
|
||||||
|
|
||||||
|
protected Integer medianGrade = 0;
|
||||||
|
|
||||||
|
protected Long medianGroupSize = 0L; // amount of judges in the median group
|
||||||
|
|
||||||
|
protected Integer contestationGrade = 0; // "best" grade of the contestation group
|
||||||
|
|
||||||
|
protected Long contestationGroupSize = 0L; // of lower grades than median
|
||||||
|
|
||||||
|
protected Integer adhesionGrade = 0; // "worst" grade of the adhesion group
|
||||||
|
|
||||||
|
protected Long adhesionGroupSize = 0L; // of higher grades than median
|
||||||
|
|
||||||
|
protected Integer secondMedianGrade = 0; // grade of the biggest group out of the median
|
||||||
|
|
||||||
|
protected Long secondMedianGroupSize = 0L; // 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 void reanalyze(ProposalTallyInterface tally) {
|
||||||
|
reanalyze(tally, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reanalyze(ProposalTallyInterface tally, Boolean favorContestation) {
|
||||||
|
this.tally = tally;
|
||||||
|
this.totalSize = 0L;
|
||||||
|
this.medianGrade = 0;
|
||||||
|
this.medianGroupSize = 0L;
|
||||||
|
this.contestationGrade = 0;
|
||||||
|
this.contestationGroupSize = 0L;
|
||||||
|
this.adhesionGrade = 0;
|
||||||
|
this.adhesionGroupSize = 0L;
|
||||||
|
this.secondMedianGrade = 0;
|
||||||
|
this.secondMedianGroupSize = 0L;
|
||||||
|
this.secondMedianGroupSign = 0;
|
||||||
|
|
||||||
|
Long[] gradesTallies = this.tally.getTally();
|
||||||
|
int amountOfGrades = gradesTallies.length;
|
||||||
|
|
||||||
|
for (int grade = 0; grade < amountOfGrades; grade++) {
|
||||||
|
Long gradeTally = gradesTallies[grade];
|
||||||
|
assert(0 <= gradeTally); // Negative tallies are not allowed.
|
||||||
|
this.totalSize += gradeTally;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer medianOffset = 1;
|
||||||
|
if ( ! favorContestation) {
|
||||||
|
medianOffset = 2;
|
||||||
|
}
|
||||||
|
Long medianCursor = (long) Math.floor((this.totalSize + medianOffset) / 2.0);
|
||||||
|
|
||||||
|
Long tallyBeforeCursor = 0L;
|
||||||
|
Long tallyCursor = 0L;
|
||||||
|
Boolean foundMedian = false;
|
||||||
|
Integer contestationGrade = 0;
|
||||||
|
Integer adhesionGrade = 0;
|
||||||
|
for (int grade = 0; grade < amountOfGrades; grade++) {
|
||||||
|
Long gradeTally = gradesTallies[grade];
|
||||||
|
tallyBeforeCursor = tallyCursor;
|
||||||
|
tallyCursor += gradeTally;
|
||||||
|
|
||||||
|
if ( ! foundMedian) {
|
||||||
|
if (tallyCursor >= medianCursor) {
|
||||||
|
foundMedian = true;
|
||||||
|
this.medianGrade = grade;
|
||||||
|
this.contestationGroupSize = tallyBeforeCursor;
|
||||||
|
this.medianGroupSize = gradeTally;
|
||||||
|
this.adhesionGroupSize = this.totalSize - this.contestationGroupSize - this.medianGroupSize;
|
||||||
|
} else {
|
||||||
|
if (0 < gradeTally) {
|
||||||
|
contestationGrade = grade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (0 < gradeTally && 0 == adhesionGrade) {
|
||||||
|
adhesionGrade = grade;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contestationGrade = contestationGrade;
|
||||||
|
this.adhesionGrade = adhesionGrade;
|
||||||
|
this.secondMedianGroupSize = Math.max(this.contestationGroupSize, this.adhesionGroupSize);
|
||||||
|
this.secondMedianGroupSign = 0;
|
||||||
|
if (this.contestationGroupSize < this.adhesionGroupSize) {
|
||||||
|
this.secondMedianGrade = this.adhesionGrade;
|
||||||
|
this.secondMedianGroupSign = 1;
|
||||||
|
} else if (this.contestationGroupSize > 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) {
|
||||||
|
this.secondMedianGroupSign = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getTotalSize() {
|
||||||
|
return totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalSize(Long totalSize) {
|
||||||
|
this.totalSize = totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getMedianGrade() {
|
||||||
|
return medianGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMedianGrade(Integer medianGrade) {
|
||||||
|
this.medianGrade = medianGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getMedianGroupSize() {
|
||||||
|
return medianGroupSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMedianGroupSize(Long medianGroupSize) {
|
||||||
|
this.medianGroupSize = medianGroupSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getContestationGrade() {
|
||||||
|
return contestationGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContestationGrade(Integer contestationGrade) {
|
||||||
|
this.contestationGrade = contestationGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getContestationGroupSize() {
|
||||||
|
return contestationGroupSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContestationGroupSize(Long contestationGroupSize) {
|
||||||
|
this.contestationGroupSize = contestationGroupSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getAdhesionGrade() {
|
||||||
|
return adhesionGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdhesionGrade(Integer adhesionGrade) {
|
||||||
|
this.adhesionGrade = adhesionGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getAdhesionGroupSize() {
|
||||||
|
return adhesionGroupSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdhesionGroupSize(Long adhesionGroupSize) {
|
||||||
|
this.adhesionGroupSize = adhesionGroupSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSecondMedianGrade() {
|
||||||
|
return secondMedianGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSecondMedianGrade(Integer secondMedianGrade) {
|
||||||
|
this.secondMedianGrade = secondMedianGrade;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSecondMedianGroupSize() {
|
||||||
|
return secondMedianGroupSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSecondMedianGroupSize(Long secondMedianGroupSize) {
|
||||||
|
this.secondMedianGroupSize = secondMedianGroupSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSecondMedianGroupSign() {
|
||||||
|
return secondMedianGroupSign;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSecondMedianGroupSign(Integer sign) {
|
||||||
|
this.secondMedianGroupSign = sign;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
public interface ProposalTallyInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The amount of judgments received for each Grade, from "worst" Grade to "best" Grade.
|
||||||
|
*/
|
||||||
|
public Long[] getTally();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
public class Tally implements TallyInterface {
|
||||||
|
|
||||||
|
protected ProposalTallyInterface[] proposalsTallies;
|
||||||
|
|
||||||
|
protected Long amountOfJudges = 0L;
|
||||||
|
|
||||||
|
public Tally(ProposalTallyInterface[] proposalsTallies, Long amountOfJudges) {
|
||||||
|
setProposalsTallies(proposalsTallies);
|
||||||
|
setAmountOfJudges(amountOfJudges);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tally(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges) {
|
||||||
|
setProposalsTallies(proposalsTallies);
|
||||||
|
setAmountOfJudges(Long.valueOf(amountOfJudges));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProposalTallyInterface[] getProposalsTallies() {
|
||||||
|
return proposalsTallies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProposalsTallies(ProposalTallyInterface[] proposalsTallies) {
|
||||||
|
this.proposalsTallies = proposalsTallies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getAmountOfProposals() {
|
||||||
|
return proposalsTallies.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getAmountOfJudges() {
|
||||||
|
return amountOfJudges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAmountOfJudges(Long amountOfJudges) {
|
||||||
|
this.amountOfJudges = amountOfJudges;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
public interface TallyInterface {
|
||||||
|
|
||||||
|
public ProposalTallyInterface[] getProposalsTallies();
|
||||||
|
|
||||||
|
public Long getAmountOfJudges();
|
||||||
|
|
||||||
|
public Integer getAmountOfProposals();
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class MajorityJudgmentDeliberatorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDemoUsage() {
|
||||||
|
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
|
||||||
|
TallyInterface tally = new Tally(new ProposalTallyInterface[] {
|
||||||
|
new ProposalTally(new Integer[]{4, 5, 2, 1, 3, 1, 2}),
|
||||||
|
new ProposalTally(new Integer[]{3, 6, 2, 1, 3, 1, 2}),
|
||||||
|
}, 18L);
|
||||||
|
ResultInterface result = mj.deliberate(tally);
|
||||||
|
|
||||||
|
// System.out.println("Score 0: "+result.getProposalResults()[0].getScore());
|
||||||
|
// System.out.println("Score 1: "+result.getProposalResults()[1].getScore());
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(2, result.getProposalResults().length);
|
||||||
|
assertEquals(2, result.getProposalResults()[0].getRank());
|
||||||
|
assertEquals(1, result.getProposalResults()[1].getRank());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUsageWithBigNumbers() {
|
||||||
|
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
|
||||||
|
TallyInterface tally = new Tally(new ProposalTallyInterface[] {
|
||||||
|
new ProposalTally(new Long[]{11312415004L, 21153652410L, 24101523299L, 18758623562L}),
|
||||||
|
new ProposalTally(new Long[]{11312415004L, 21153652400L, 24101523299L, 18758623572L}),
|
||||||
|
// new ProposalTally(new Long[]{14526586452L, 40521123260L, 14745623120L, 40526235129L}),
|
||||||
|
}, 75326214275L);
|
||||||
|
ResultInterface result = mj.deliberate(tally);
|
||||||
|
|
||||||
|
// System.out.println("Score 0: "+result.getProposalResults()[0].getScore());
|
||||||
|
// System.out.println("Score 1: "+result.getProposalResults()[1].getScore());
|
||||||
|
// System.out.println("Total "+(11312415004L+21153652410L+24101523299L+18758623562L));
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(2, result.getProposalResults().length);
|
||||||
|
assertEquals(2, result.getProposalResults()[0].getRank());
|
||||||
|
assertEquals(1, result.getProposalResults()[1].getRank());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
package fr.mieuxvoter.mj;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
|
|
||||||
|
// CTRL+F11 in Eclipse to run
|
||||||
|
|
||||||
|
class ProposalTallyAnalysisTest {
|
||||||
|
|
||||||
|
@DisplayName("Test the proposal tally analysis")
|
||||||
|
@ParameterizedTest(name="#{index} {0} ; tally = {1}")
|
||||||
|
@MethodSource("testProvider")
|
||||||
|
void test(
|
||||||
|
String testName,
|
||||||
|
Integer[] rawTally,
|
||||||
|
Integer medianGrade,
|
||||||
|
Long medianGroupSize,
|
||||||
|
Integer contestationGrade,
|
||||||
|
Long contestationGroupSize,
|
||||||
|
Integer adhesionGrade,
|
||||||
|
Long adhesionGroupSize,
|
||||||
|
Integer secondMedianGrade,
|
||||||
|
Long secondMedianGroupSize,
|
||||||
|
Integer secondMedianGroupSign
|
||||||
|
) {
|
||||||
|
ProposalTally tally = new ProposalTally(rawTally);
|
||||||
|
ProposalTallyAnalysis pta = new ProposalTallyAnalysis(tally);
|
||||||
|
assertEquals(medianGrade, pta.getMedianGrade(), "Median Grade");
|
||||||
|
assertEquals(medianGroupSize, pta.getMedianGroupSize(), "Median Group Size");
|
||||||
|
assertEquals(contestationGrade, pta.getContestationGrade(), "Contestation Grade");
|
||||||
|
assertEquals(contestationGroupSize, pta.getContestationGroupSize(), "Contestation Group Size");
|
||||||
|
assertEquals(adhesionGrade, pta.getAdhesionGrade(), "Adhesion Grade");
|
||||||
|
assertEquals(adhesionGroupSize, pta.getAdhesionGroupSize(), "Adhesion Group Size");
|
||||||
|
assertEquals(secondMedianGrade, pta.getSecondMedianGrade(), "Second Median Grade");
|
||||||
|
assertEquals(secondMedianGroupSize, pta.getSecondMedianGroupSize(), "Second Median Group Size");
|
||||||
|
assertEquals(secondMedianGroupSign, pta.getSecondMedianGroupSign(), "Second Median Group Sign");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Stream<Arguments> testProvider() {
|
||||||
|
return Stream.of(
|
||||||
|
// Arguments.of(
|
||||||
|
// /* name */ "Void tallies yield ???", // perhaps raise ? later
|
||||||
|
// /* tally */ new Integer[]{},
|
||||||
|
// /* medianGrade */ 0,
|
||||||
|
// /* medianGroupSize */ 0,
|
||||||
|
// /* contestationGrade */ 0,
|
||||||
|
// /* contestationGroupSize */ 0,
|
||||||
|
// /* adhesionGrade */ 0,
|
||||||
|
// /* adhesionGroupSize */ 0,
|
||||||
|
// /* secondMedianGrade */ 0,
|
||||||
|
// /* secondMedianGroupSize */ 0,
|
||||||
|
// /* secondMedianGroupSign */ 0
|
||||||
|
// ),
|
||||||
|
Arguments.of(
|
||||||
|
/* name */ "Very empty tallies yield zeroes",
|
||||||
|
/* tally */ new Integer[]{ 0 },
|
||||||
|
/* medianGrade */ 0,
|
||||||
|
/* medianGroupSize */ 0L,
|
||||||
|
/* contestationGrade */ 0,
|
||||||
|
/* contestationGroupSize */ 0L,
|
||||||
|
/* adhesionGrade */ 0,
|
||||||
|
/* adhesionGroupSize */ 0L,
|
||||||
|
/* secondMedianGrade */ 0,
|
||||||
|
/* secondMedianGroupSize */ 0L,
|
||||||
|
/* secondMedianGroupSign */ 0
|
||||||
|
),
|
||||||
|
Arguments.of(
|
||||||
|
/* name */ "Empty tallies yield zeroes",
|
||||||
|
/* tally */ new Integer[]{ 0, 0, 0, 0 },
|
||||||
|
/* medianGrade */ 0,
|
||||||
|
/* medianGroupSize */ 0L,
|
||||||
|
/* contestationGrade */ 0,
|
||||||
|
/* contestationGroupSize */ 0L,
|
||||||
|
/* adhesionGrade */ 0,
|
||||||
|
/* adhesionGroupSize */ 0L,
|
||||||
|
/* secondMedianGrade */ 0,
|
||||||
|
/* secondMedianGroupSize */ 0L,
|
||||||
|
/* secondMedianGroupSign */ 0
|
||||||
|
),
|
||||||
|
Arguments.of(
|
||||||
|
/* name */ "Absurd tally of 1 Grade",
|
||||||
|
/* tally */ new Integer[]{ 7 },
|
||||||
|
/* medianGrade */ 0,
|
||||||
|
/* medianGroupSize */ 7L,
|
||||||
|
/* contestationGrade */ 0,
|
||||||
|
/* contestationGroupSize */ 0L,
|
||||||
|
/* adhesionGrade */ 0,
|
||||||
|
/* adhesionGroupSize */ 0L,
|
||||||
|
/* secondMedianGrade */ 0,
|
||||||
|
/* secondMedianGroupSize */ 0L,
|
||||||
|
/* secondMedianGroupSign */ 0
|
||||||
|
),
|
||||||
|
Arguments.of(
|
||||||
|
/* name */ "Approbation",
|
||||||
|
/* tally */ new Integer[]{ 31, 72 },
|
||||||
|
/* medianGrade */ 1,
|
||||||
|
/* medianGroupSize */ 72L,
|
||||||
|
/* contestationGrade */ 0,
|
||||||
|
/* contestationGroupSize */ 31L,
|
||||||
|
/* adhesionGrade */ 0,
|
||||||
|
/* adhesionGroupSize */ 0L,
|
||||||
|
/* secondMedianGrade */ 0,
|
||||||
|
/* secondMedianGroupSize */ 31L,
|
||||||
|
/* secondMedianGroupSign */ -1
|
||||||
|
),
|
||||||
|
Arguments.of(
|
||||||
|
/* name */ "Equality favors contestation",
|
||||||
|
/* tally */ new Integer[]{ 42, 42 },
|
||||||
|
/* medianGrade */ 0,
|
||||||
|
/* medianGroupSize */ 42L,
|
||||||
|
/* contestationGrade */ 0,
|
||||||
|
/* contestationGroupSize */ 0L,
|
||||||
|
/* adhesionGrade */ 1,
|
||||||
|
/* adhesionGroupSize */ 42L,
|
||||||
|
/* secondMedianGrade */ 1,
|
||||||
|
/* secondMedianGroupSize */ 42L,
|
||||||
|
/* secondMedianGroupSign */ 1
|
||||||
|
),
|
||||||
|
Arguments.of(
|
||||||
|
/* name */ "Example with seven grades",
|
||||||
|
/* tally */ new Integer[]{ 4, 2, 0, 1, 2, 2, 3 },
|
||||||
|
/* medianGrade */ 3,
|
||||||
|
/* medianGroupSize */ 1L,
|
||||||
|
/* contestationGrade */ 1,
|
||||||
|
/* contestationGroupSize */ 6L,
|
||||||
|
/* adhesionGrade */ 4,
|
||||||
|
/* adhesionGroupSize */ 7L,
|
||||||
|
/* secondMedianGrade */ 4,
|
||||||
|
/* secondMedianGroupSize */ 7L,
|
||||||
|
/* secondMedianGroupSign */ 1
|
||||||
|
),
|
||||||
|
Arguments.of(
|
||||||
|
/* name */ "Works even if multiple grades are at zero",
|
||||||
|
/* tally */ new Integer[]{ 4, 0, 0, 1, 0, 0, 4 },
|
||||||
|
/* medianGrade */ 3,
|
||||||
|
/* medianGroupSize */ 1L,
|
||||||
|
/* contestationGrade */ 0,
|
||||||
|
/* contestationGroupSize */ 4L,
|
||||||
|
/* adhesionGrade */ 6,
|
||||||
|
/* adhesionGroupSize */ 4L,
|
||||||
|
/* secondMedianGrade */ 0,
|
||||||
|
/* secondMedianGroupSize */ 4L,
|
||||||
|
/* secondMedianGroupSign */ -1
|
||||||
|
),
|
||||||
|
Arguments.of(
|
||||||
|
/* name */ "Weird tally",
|
||||||
|
/* tally */ new Integer[]{ 1, 1, 1, 1, 1, 1, 1 },
|
||||||
|
/* medianGrade */ 3,
|
||||||
|
/* medianGroupSize */ 1L,
|
||||||
|
/* contestationGrade */ 2,
|
||||||
|
/* contestationGroupSize */ 3L,
|
||||||
|
/* adhesionGrade */ 4,
|
||||||
|
/* adhesionGroupSize */ 3L,
|
||||||
|
/* secondMedianGrade */ 2,
|
||||||
|
/* secondMedianGroupSize */ 3L,
|
||||||
|
/* secondMedianGroupSign */ -1
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue