Merge pull request #12 from MieuxVoter/feat-median-default-grade

Implement median default grade tally
pull/14/head
Dominique Merle 3 years ago committed by GitHub
commit d542567cb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -140,17 +140,17 @@ ResultInterface result = mj.deliberate(tally);
- [x] Ranking - [x] Ranking
- [x] Release v0.1.0 - [x] Release v0.1.0
- [x] Guess the amount of judges - [x] Guess the amount of judges
- [ ] Allow defining a default grade - [x] Allow defining a default grade
- [x] Static Grade (configurable) - [x] Static Grade (configurable)
- [x] Normalization (using least common multiple) - [x] Normalization (using Least Common Multiple)
- [ ] Median Grade - [x] Median Grade
- [ ] Release v0.2.0 - [ ] Release v0.2.0
- [ ] Publish on package repositories - [ ] Publish on package repositories
- [ ] Gradle - [ ] Gradle
- [ ] Maven - [ ] Maven
- [ ] … ? (please share your knowledge to help us!) - [ ] … ? (please share your knowledge to help us!)
- [ ] Release v0.3.0 - [ ] 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 - [ ] Release v1.0.0

@ -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);
// <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);
}
}
}
}

@ -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();
}
}

@ -2,9 +2,27 @@ package fr.mieuxvoter.mj;
import java.math.BigInteger; 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; 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) { public TallyWithDefaultGrade(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges, Integer defaultGrade) {
super(proposalsTallies, amountOfJudges); super(proposalsTallies, amountOfJudges);
@ -17,25 +35,16 @@ public class TallyWithDefaultGrade extends Tally implements TallyInterface {
this.defaultGrade = defaultGrade; this.defaultGrade = defaultGrade;
fillWithDefaultGrade(); fillWithDefaultGrade();
} }
public TallyWithDefaultGrade(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges, Integer defaultGrade) { public TallyWithDefaultGrade(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges, Integer defaultGrade) {
super(proposalsTallies, amountOfJudges); super(proposalsTallies, amountOfJudges);
this.defaultGrade = defaultGrade; this.defaultGrade = defaultGrade;
fillWithDefaultGrade(); fillWithDefaultGrade();
} }
protected void fillWithDefaultGrade() { @Override
int amountOfProposals = getAmountOfProposals(); protected Integer getDefaultGradeForProposal(ProposalTallyInterface proposalTally) {
for (int i = 0 ; i < amountOfProposals ; i++) { return this.defaultGrade;
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);
}
}
} }
} }

@ -20,6 +20,9 @@ class MajorityJudgmentDeliberatorTest {
@ParameterizedTest(name="#{index} {0}") @ParameterizedTest(name="#{index} {0}")
@JsonFileSource(resources = "/assertions.json") @JsonFileSource(resources = "/assertions.json")
public void testFromJson(JsonObject datum) { 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"); JsonArray jsonTallies = datum.getJsonArray("tallies");
int amountOfProposals = jsonTallies.size(); int amountOfProposals = jsonTallies.size();
BigInteger amountOfParticipants = new BigInteger(datum.get("participants").toString()); BigInteger amountOfParticipants = new BigInteger(datum.get("participants").toString());
@ -39,6 +42,8 @@ class MajorityJudgmentDeliberatorTest {
TallyInterface tally; TallyInterface tally;
if ("StaticDefault".equalsIgnoreCase(mode)) { if ("StaticDefault".equalsIgnoreCase(mode)) {
tally = new TallyWithDefaultGrade(tallies, amountOfParticipants, datum.getInt("default")); tally = new TallyWithDefaultGrade(tallies, amountOfParticipants, datum.getInt("default"));
} else if ("MedianDefault".equalsIgnoreCase(mode)) {
tally = new MedianDefaultTally(tallies, amountOfParticipants);
} else if ("Normalized".equalsIgnoreCase(mode)) { } else if ("Normalized".equalsIgnoreCase(mode)) {
tally = new NormalizedTally(tallies); tally = new NormalizedTally(tallies);
} else { } else {
@ -213,7 +218,7 @@ class MajorityJudgmentDeliberatorTest {
} }
@Test @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() { public void testStaticDefaultWithThousandsOfProposals() {
int amountOfProposals = 1337; int amountOfProposals = 1337;
Integer amountOfJudges = 60000000; 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 @Test
@DisplayName("Test normalized tallies with thousands of (prime) proposals") @DisplayName("Test normalized tallies with thousands of (prime) proposals")
public void testNormalizedWithThousandsOfPrimeProposals() { public void testNormalizedWithThousandsOfPrimeProposals() {
// We're using primes to test the upper bounds of our LCM shenanigans. // We're using primes to test the upper bounds of our LCM shenanigans.
// This test takes a long time! (3 seconds) // This test takes a long time! (3 seconds)
// List<Integer> generatedPrimes = sieveOfEratosthenes(15000);
// System.out.println(generatedPrimes);
int amountOfProposals = primes.length; // 1437 int amountOfProposals = primes.length; // 1437
DeliberatorInterface mj = new MajorityJudgmentDeliberator(); DeliberatorInterface mj = new MajorityJudgmentDeliberator();
ProposalTallyInterface[] tallies = new ProposalTallyInterface[amountOfProposals]; ProposalTallyInterface[] tallies = new ProposalTallyInterface[amountOfProposals];
@ -282,7 +308,7 @@ class MajorityJudgmentDeliberatorTest {
assertEquals(amountOfProposals, result.getProposalResults().length); assertEquals(amountOfProposals, result.getProposalResults().length);
for (int i = 0 ; i < amountOfProposals ; i++) { for (int i = 0 ; i < amountOfProposals ; i++) {
assertEquals( assertEquals(
1 + (i % primes.length), result.getProposalResults()[i].getRank(), 1 + i, result.getProposalResults()[i].getRank(),
"Rank of Proposal #" + i "Rank of Proposal #" + i
); );
} }
@ -292,7 +318,7 @@ class MajorityJudgmentDeliberatorTest {
@DisplayName("Test normalized tallies with thousands of proposals") @DisplayName("Test normalized tallies with thousands of proposals")
public void testNormalizedWithThousandsOfProposals() { public void testNormalizedWithThousandsOfProposals() {
// This test is faster than the primes one (0.4 seconds), // 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 int amountOfProposals = primes.length; // 1437
DeliberatorInterface mj = new MajorityJudgmentDeliberator(); DeliberatorInterface mj = new MajorityJudgmentDeliberator();
@ -309,12 +335,14 @@ class MajorityJudgmentDeliberatorTest {
assertEquals(amountOfProposals, result.getProposalResults().length); assertEquals(amountOfProposals, result.getProposalResults().length);
for (int i = 0 ; i < amountOfProposals ; i++) { for (int i = 0 ; i < amountOfProposals ; i++) {
assertEquals( assertEquals(
1 + (i % primes.length), result.getProposalResults()[i].getRank(), 1 + i, result.getProposalResults()[i].getRank(),
"Rank of Proposal #" + i "Rank of Proposal #" + i
); );
} }
} }
// …
// @Test // @Test
// public void runBenchmarks() throws Exception { // public void runBenchmarks() throws Exception {
// Options options = new OptionsBuilder() // Options options = new OptionsBuilder()

@ -4,11 +4,11 @@
"title": "Few participants", "title": "Few participants",
"participants": 3, "participants": 3,
"tallies": [ "tallies": [
[1, 1, 1], [ 1, 1, 1 ],
[1, 0, 2], [ 1, 0, 2 ],
[3, 0, 0], [ 3, 0, 0 ],
[2, 0, 1], [ 2, 0, 1 ],
[0, 3, 0] [ 0, 3, 0 ]
], ],
"ranks": [ "ranks": [
3, 3,
@ -22,9 +22,9 @@
"title": "Thousands of participants", "title": "Thousands of participants",
"participants": 37000, "participants": 37000,
"tallies": [ "tallies": [
[11142, 6970, 4040, 1968, 9888, 2992], [ 11142, 6970, 4040, 1968, 9888, 2992 ],
[10141, 8971, 4043, 1965, 8884, 2996], [ 10141, 8971, 4043, 1965, 8884, 2996 ],
[14141, 8971, 1043, 1965, 7884, 2996] [ 14141, 8971, 1043, 1965, 7884, 2996 ]
], ],
"ranks": [ "ranks": [
1, 1,
@ -36,11 +36,11 @@
"title": "Millions of participants", "title": "Millions of participants",
"participants": 72327456, "participants": 72327456,
"tallies": [ "tallies": [
[5272679, 19797001, 10732688, 9612936, 1379840, 16886281, 8646031], [ 5272679, 19797001, 10732688, 9612936, 1379840, 16886281, 8646031 ],
[16354546, 11690342, 9451800, 14245973, 817593, 12461162, 7306040], [ 16354546, 11690342, 9451800, 14245973, 817593, 12461162, 7306040 ],
[9849171, 17970690, 14276861, 4606692, 16404594, 6760147, 2459301], [ 9849171, 17970690, 14276861, 4606692, 16404594, 6760147, 2459301 ],
[2645563, 12907474, 1278331, 22843261, 8025412, 8964952, 15662463], [ 2645563, 12907474, 1278331, 22843261, 8025412, 8964952, 15662463 ],
[16293252, 12277630, 38348, 14929905, 11087753, 10634266, 7066302] [ 16293252, 12277630, 38348, 14929905, 11087753, 10634266, 7066302 ]
], ],
"ranks": [ "ranks": [
3, 3,
@ -90,6 +90,25 @@
2 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", "title": "Normalization",
"participants": 10, "participants": 10,

Loading…
Cancel
Save