diff --git a/README.md b/README.md index 681237e..0f0c9d1 100644 --- a/README.md +++ b/README.md @@ -140,17 +140,17 @@ ResultInterface result = mj.deliberate(tally); - [x] Ranking - [x] Release v0.1.0 - [x] Guess the amount of judges -- [ ] Allow defining a default grade +- [x] Allow defining a default grade - [x] Static Grade (configurable) - - [x] Normalization (using least common multiple) - - [ ] Median Grade + - [x] Normalization (using Least Common Multiple) + - [x] Median Grade - [ ] Release v0.2.0 - [ ] Publish on package repositories - [ ] Gradle - [ ] 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) +- [ ] Use it somewhere in an application, adjust API as needed (one last time) - [ ] Release v1.0.0 diff --git a/src/main/java/fr/mieuxvoter/mj/DefaultGradeTally.java b/src/main/java/fr/mieuxvoter/mj/DefaultGradeTally.java new file mode 100644 index 0000000..bf9162d --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/DefaultGradeTally.java @@ -0,0 +1,50 @@ +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. + */ +abstract public class DefaultGradeTally extends Tally implements TallyInterface { + + /** + * Override this to choose the default grade for a given proposal. + */ + abstract protected Integer getDefaultGradeForProposal(ProposalTallyInterface proposalTally); + + // /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); + } + } + } + +} diff --git a/src/main/java/fr/mieuxvoter/mj/MedianDefaultTally.java b/src/main/java/fr/mieuxvoter/mj/MedianDefaultTally.java new file mode 100644 index 0000000..5379590 --- /dev/null +++ b/src/main/java/fr/mieuxvoter/mj/MedianDefaultTally.java @@ -0,0 +1,38 @@ +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(); + } + +} diff --git a/src/main/java/fr/mieuxvoter/mj/TallyWithDefaultGrade.java b/src/main/java/fr/mieuxvoter/mj/TallyWithDefaultGrade.java index f6e458e..8cdd93a 100644 --- a/src/main/java/fr/mieuxvoter/mj/TallyWithDefaultGrade.java +++ b/src/main/java/fr/mieuxvoter/mj/TallyWithDefaultGrade.java @@ -2,9 +2,27 @@ package fr.mieuxvoter.mj; import java.math.BigInteger; -public class TallyWithDefaultGrade extends Tally implements TallyInterface { +public class TallyWithDefaultGrade 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. + * + * Example: + * + * 0 == REJECT + * 1 == PASSABLE + * 2 == GOOD + * 3 == EXCELLENT + */ protected Integer defaultGrade = 0; + + public TallyWithDefaultGrade(TallyInterface tally, Integer defaultGrade) { + super(tally.getProposalsTallies(), tally.getAmountOfJudges()); + this.defaultGrade = defaultGrade; + fillWithDefaultGrade(); + } public TallyWithDefaultGrade(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges, Integer defaultGrade) { super(proposalsTallies, amountOfJudges); @@ -17,25 +35,16 @@ public class TallyWithDefaultGrade extends Tally implements TallyInterface { this.defaultGrade = defaultGrade; fillWithDefaultGrade(); } - + public TallyWithDefaultGrade(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges, Integer defaultGrade) { super(proposalsTallies, amountOfJudges); this.defaultGrade = defaultGrade; fillWithDefaultGrade(); } - - protected void fillWithDefaultGrade() { - int amountOfProposals = getAmountOfProposals(); - for (int i = 0 ; i < amountOfProposals ; i++) { - ProposalTallyInterface proposal = getProposalsTallies()[i]; - BigInteger amountOfJudgments = proposal.getAmountOfJudgments(); - BigInteger missingAmount = this.amountOfJudges.subtract(amountOfJudgments); - int missingSign = missingAmount.compareTo(BigInteger.ZERO); - assert(0 <= missingSign); // ERROR: More judgments than judges! - if (0 < missingSign) { - proposal.getTally()[this.defaultGrade] = proposal.getTally()[this.defaultGrade].add(missingAmount); - } - } + + @Override + protected Integer getDefaultGradeForProposal(ProposalTallyInterface proposalTally) { + return this.defaultGrade; } } diff --git a/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java b/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java index 68fadb8..fb909c6 100644 --- a/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java +++ b/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java @@ -20,6 +20,9 @@ class MajorityJudgmentDeliberatorTest { @ParameterizedTest(name="#{index} {0}") @JsonFileSource(resources = "/assertions.json") public void testFromJson(JsonObject datum) { + // This test uses the JSON file in test/resources/ + // It also allows testing the various modes of default grades. + JsonArray jsonTallies = datum.getJsonArray("tallies"); int amountOfProposals = jsonTallies.size(); BigInteger amountOfParticipants = new BigInteger(datum.get("participants").toString()); @@ -39,6 +42,8 @@ class MajorityJudgmentDeliberatorTest { TallyInterface tally; if ("StaticDefault".equalsIgnoreCase(mode)) { tally = new TallyWithDefaultGrade(tallies, amountOfParticipants, datum.getInt("default")); + } else if ("MedianDefault".equalsIgnoreCase(mode)) { + tally = new MedianDefaultTally(tallies, amountOfParticipants); } else if ("Normalized".equalsIgnoreCase(mode)) { tally = new NormalizedTally(tallies); } else { @@ -213,7 +218,7 @@ class MajorityJudgmentDeliberatorTest { } @Test - @DisplayName("Test static default grade with thousands of proposals") + @DisplayName("Test static default grade with thousands of proposals and millions of judges") public void testStaticDefaultWithThousandsOfProposals() { int amountOfProposals = 1337; Integer amountOfJudges = 60000000; @@ -257,15 +262,36 @@ class MajorityJudgmentDeliberatorTest { // } // } + @Test + @DisplayName("Test with a median default grade") + public void testMedianDefaultGrade() { + Integer amountOfJudges = 42; + DeliberatorInterface mj = new MajorityJudgmentDeliberator(); + TallyInterface tally = new MedianDefaultTally(new ProposalTallyInterface[] { + new ProposalTally(new Integer[]{ 0, 0, 1 }), + new ProposalTally(new Integer[]{ 0, 1, 0 }), + new ProposalTally(new Integer[]{ 1, 1, 1 }), + new ProposalTally(new Integer[]{ 1, 0, 1 }), + new ProposalTally(new Integer[]{ 1, 0, 0 }), + }, amountOfJudges); + + ResultInterface result = mj.deliberate(tally); + + assertNotNull(result); + assertEquals(5, result.getProposalResults().length); + assertEquals(1, result.getProposalResults()[0].getRank()); + assertEquals(2, result.getProposalResults()[1].getRank()); + assertEquals(3, result.getProposalResults()[2].getRank()); + assertEquals(4, result.getProposalResults()[3].getRank()); + assertEquals(5, result.getProposalResults()[4].getRank()); + } + @Test @DisplayName("Test normalized tallies with thousands of (prime) proposals") public void testNormalizedWithThousandsOfPrimeProposals() { // We're using primes to test the upper bounds of our LCM shenanigans. // This test takes a long time! (3 seconds) -// List generatedPrimes = sieveOfEratosthenes(15000); -// System.out.println(generatedPrimes); - int amountOfProposals = primes.length; // 1437 DeliberatorInterface mj = new MajorityJudgmentDeliberator(); ProposalTallyInterface[] tallies = new ProposalTallyInterface[amountOfProposals]; @@ -282,7 +308,7 @@ class MajorityJudgmentDeliberatorTest { assertEquals(amountOfProposals, result.getProposalResults().length); for (int i = 0 ; i < amountOfProposals ; i++) { assertEquals( - 1 + (i % primes.length), result.getProposalResults()[i].getRank(), + 1 + i, result.getProposalResults()[i].getRank(), "Rank of Proposal #" + i ); } @@ -292,7 +318,7 @@ class MajorityJudgmentDeliberatorTest { @DisplayName("Test normalized tallies with thousands of proposals") public void testNormalizedWithThousandsOfProposals() { // This test is faster than the primes one (0.4 seconds), - // since primes are the worst case-scenario for our LCM. + // since primes are the worst-case scenario for our LCM. int amountOfProposals = primes.length; // 1437 DeliberatorInterface mj = new MajorityJudgmentDeliberator(); @@ -309,12 +335,14 @@ class MajorityJudgmentDeliberatorTest { assertEquals(amountOfProposals, result.getProposalResults().length); for (int i = 0 ; i < amountOfProposals ; i++) { assertEquals( - 1 + (i % primes.length), result.getProposalResults()[i].getRank(), + 1 + i, result.getProposalResults()[i].getRank(), "Rank of Proposal #" + i ); } } + // … + // @Test // public void runBenchmarks() throws Exception { // Options options = new OptionsBuilder() diff --git a/src/test/resources/assertions.json b/src/test/resources/assertions.json index c3fadc5..d194a8a 100644 --- a/src/test/resources/assertions.json +++ b/src/test/resources/assertions.json @@ -4,11 +4,11 @@ "title": "Few participants", "participants": 3, "tallies": [ - [1, 1, 1], - [1, 0, 2], - [3, 0, 0], - [2, 0, 1], - [0, 3, 0] + [ 1, 1, 1 ], + [ 1, 0, 2 ], + [ 3, 0, 0 ], + [ 2, 0, 1 ], + [ 0, 3, 0 ] ], "ranks": [ 3, @@ -22,9 +22,9 @@ "title": "Thousands of participants", "participants": 37000, "tallies": [ - [11142, 6970, 4040, 1968, 9888, 2992], - [10141, 8971, 4043, 1965, 8884, 2996], - [14141, 8971, 1043, 1965, 7884, 2996] + [ 11142, 6970, 4040, 1968, 9888, 2992 ], + [ 10141, 8971, 4043, 1965, 8884, 2996 ], + [ 14141, 8971, 1043, 1965, 7884, 2996 ] ], "ranks": [ 1, @@ -36,11 +36,11 @@ "title": "Millions of participants", "participants": 72327456, "tallies": [ - [5272679, 19797001, 10732688, 9612936, 1379840, 16886281, 8646031], - [16354546, 11690342, 9451800, 14245973, 817593, 12461162, 7306040], - [9849171, 17970690, 14276861, 4606692, 16404594, 6760147, 2459301], - [2645563, 12907474, 1278331, 22843261, 8025412, 8964952, 15662463], - [16293252, 12277630, 38348, 14929905, 11087753, 10634266, 7066302] + [ 5272679, 19797001, 10732688, 9612936, 1379840, 16886281, 8646031 ], + [ 16354546, 11690342, 9451800, 14245973, 817593, 12461162, 7306040 ], + [ 9849171, 17970690, 14276861, 4606692, 16404594, 6760147, 2459301 ], + [ 2645563, 12907474, 1278331, 22843261, 8025412, 8964952, 15662463 ], + [ 16293252, 12277630, 38348, 14929905, 11087753, 10634266, 7066302 ] ], "ranks": [ 3, @@ -90,6 +90,25 @@ 2 ] }, + { + "title": "Median Default Grade", + "participants": 10, + "mode": "MedianDefault", + "tallies": [ + [ 2, 2, 2, 2, 2 ], + [ 2, 2, 1, 2, 2 ], + [ 0, 0, 4, 0, 0 ], + [ 0, 0, 0, 0, 2 ], + [ 0, 0, 0, 1, 1 ] + ], + "ranks": [ + 4, + 4, + 3, + 1, + 2 + ] + }, { "title": "Normalization", "participants": 10,