commit 7d9b24927ed8d30bbb83a0921c7f50ef40c307d6 Author: domi41 Date: Sun Apr 25 07:47:54 2021 +0200 feat: implement a majority judgment deliberator 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 36h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b65256 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.settings +/.classpath +/.project +/.gradle + +/build + +*.class diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5e25f8f --- /dev/null +++ b/LICENSE.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b95a6a --- /dev/null +++ b/README.md @@ -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 + diff --git a/src/main/java/fr/mieuxvoter/mj/DeliberatorInterface.java b/src/main/java/fr/mieuxvoter/mj/DeliberatorInterface.java new file mode 100644 index 0000000..ec93cda --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/DeliberatorInterface.java @@ -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); +} diff --git a/src/main/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberator.java b/src/main/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberator.java new file mode 100644 index 0000000..e408159 --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberator.java @@ -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() { + @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; + } + +} diff --git a/src/main/java/fr/mieuxvoter/mj/ProposalResult.java b/src/main/java/fr/mieuxvoter/mj/ProposalResult.java new file mode 100644 index 0000000..0b70813 --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/ProposalResult.java @@ -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; + } + +} diff --git a/src/main/java/fr/mieuxvoter/mj/ProposalResultInterface.java b/src/main/java/fr/mieuxvoter/mj/ProposalResultInterface.java new file mode 100644 index 0000000..692ac61 --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/ProposalResultInterface.java @@ -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(); + +} diff --git a/src/main/java/fr/mieuxvoter/mj/ProposalTally.java b/src/main/java/fr/mieuxvoter/mj/ProposalTally.java new file mode 100644 index 0000000..dc14602 --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/ProposalTally.java @@ -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; + } + +} diff --git a/src/main/java/fr/mieuxvoter/mj/ProposalTallyAnalysis.java b/src/main/java/fr/mieuxvoter/mj/ProposalTallyAnalysis.java new file mode 100644 index 0000000..8fa71bf --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/ProposalTallyAnalysis.java @@ -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; + } + +} diff --git a/src/main/java/fr/mieuxvoter/mj/ProposalTallyInterface.java b/src/main/java/fr/mieuxvoter/mj/ProposalTallyInterface.java new file mode 100644 index 0000000..9f4b6c6 --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/ProposalTallyInterface.java @@ -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); + +} diff --git a/src/main/java/fr/mieuxvoter/mj/Result.java b/src/main/java/fr/mieuxvoter/mj/Result.java new file mode 100644 index 0000000..76b1a4c --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/Result.java @@ -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; + } + +} diff --git a/src/main/java/fr/mieuxvoter/mj/ResultInterface.java b/src/main/java/fr/mieuxvoter/mj/ResultInterface.java new file mode 100644 index 0000000..79c011a --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/ResultInterface.java @@ -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(); + +} diff --git a/src/main/java/fr/mieuxvoter/mj/Tally.java b/src/main/java/fr/mieuxvoter/mj/Tally.java new file mode 100644 index 0000000..38e1a2f --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/Tally.java @@ -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; + } + +} diff --git a/src/main/java/fr/mieuxvoter/mj/TallyInterface.java b/src/main/java/fr/mieuxvoter/mj/TallyInterface.java new file mode 100644 index 0000000..308473f --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/TallyInterface.java @@ -0,0 +1,11 @@ +package fr.mieuxvoter.mj; + +public interface TallyInterface { + + public ProposalTallyInterface[] getProposalsTallies(); + + public Long getAmountOfJudges(); + + public Integer getAmountOfProposals(); + +} diff --git a/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java b/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java new file mode 100644 index 0000000..ba65cf4 --- /dev/null +++ b/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java @@ -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()); + } + +} diff --git a/src/test/java/fr/mieuxvoter/mj/ProposalTallyAnalysisTest.java b/src/test/java/fr/mieuxvoter/mj/ProposalTallyAnalysisTest.java new file mode 100644 index 0000000..e7890f6 --- /dev/null +++ b/src/test/java/fr/mieuxvoter/mj/ProposalTallyAnalysisTest.java @@ -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 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 + ) + ); + } +}